@furystack/security 7.0.9 → 8.0.1
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 +21 -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/errors/password-complexity-error.d.ts +5 -4
- package/esm/errors/password-complexity-error.d.ts.map +1 -1
- package/esm/errors/password-complexity-error.js +5 -4
- package/esm/errors/password-complexity-error.js.map +1 -1
- package/esm/errors/unauthenticated-error.d.ts +3 -1
- package/esm/errors/unauthenticated-error.d.ts.map +1 -1
- package/esm/errors/unauthenticated-error.js +3 -1
- package/esm/errors/unauthenticated-error.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/models/password-credential.d.ts +3 -13
- package/esm/models/password-credential.d.ts.map +1 -1
- package/esm/models/password-credential.js +3 -1
- package/esm/models/password-credential.js.map +1 -1
- package/esm/models/password-reset-token.d.ts +5 -9
- package/esm/models/password-reset-token.d.ts.map +1 -1
- package/esm/models/password-reset-token.js +5 -0
- package/esm/models/password-reset-token.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-complexity-rules/contains-lowercase.d.ts +2 -2
- package/esm/password-complexity-rules/contains-lowercase.js +2 -2
- package/esm/password-complexity-rules/contains-uppercase.d.ts +2 -2
- package/esm/password-complexity-rules/contains-uppercase.js +2 -2
- package/esm/password-complexity-rules/max-length.d.ts +1 -4
- package/esm/password-complexity-rules/max-length.d.ts.map +1 -1
- package/esm/password-complexity-rules/max-length.js +1 -4
- package/esm/password-complexity-rules/max-length.js.map +1 -1
- package/esm/password-complexity-rules/min-length.d.ts +1 -4
- package/esm/password-complexity-rules/min-length.d.ts.map +1 -1
- package/esm/password-complexity-rules/min-length.js +1 -4
- package/esm/password-complexity-rules/min-length.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 +7 -7
- package/src/crypto-password-hasher.ts +46 -39
- package/src/errors/password-complexity-error.ts +7 -4
- package/src/errors/unauthenticated-error.ts +3 -1
- package/src/helpers.ts +27 -15
- package/src/index.ts +3 -2
- package/src/models/password-credential.ts +5 -15
- package/src/models/password-reset-token.ts +5 -9
- package/src/password-authenticator.spec.ts +134 -254
- package/src/password-authenticator.ts +92 -86
- package/src/password-complexity-rules/contains-lowercase.ts +2 -2
- package/src/password-complexity-rules/contains-uppercase.ts +2 -2
- package/src/password-complexity-rules/max-length.ts +1 -4
- package/src/password-complexity-rules/min-length.ts +1 -4
- 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/esm/security-policy.js
CHANGED
|
@@ -1,35 +1,25 @@
|
|
|
1
|
-
|
|
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
|
-
import { Injectable } from '@furystack/inject';
|
|
1
|
+
import { defineService } from '@furystack/inject';
|
|
8
2
|
import { CryptoPasswordHasher } from './crypto-password-hasher.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
};
|
|
31
|
-
SecurityPolicy = __decorate([
|
|
32
|
-
Injectable({ lifetime: 'singleton' })
|
|
33
|
-
], SecurityPolicy);
|
|
34
|
-
export { SecurityPolicy };
|
|
3
|
+
/**
|
|
4
|
+
* Returns a fresh copy of the default {@link SecurityPolicy}. Useful as a
|
|
5
|
+
* starting point for `Object.assign`-style overrides or spec fixtures.
|
|
6
|
+
*/
|
|
7
|
+
export const defaultSecurityPolicy = () => ({
|
|
8
|
+
hasher: CryptoPasswordHasher,
|
|
9
|
+
passwordComplexityRules: [],
|
|
10
|
+
passwordExpirationDays: 0,
|
|
11
|
+
requireSecondFactor: 'never',
|
|
12
|
+
resetTokenExpirationSeconds: 15,
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* DI token that resolves the current {@link SecurityPolicy}. Rebind this
|
|
16
|
+
* token via {@link usePasswordPolicy} (preferred) or directly through
|
|
17
|
+
* {@link Injector.bind} to customise the policy per-application or
|
|
18
|
+
* per-scope.
|
|
19
|
+
*/
|
|
20
|
+
export const SecurityPolicy = defineService({
|
|
21
|
+
name: 'furystack/security/SecurityPolicy',
|
|
22
|
+
lifetime: 'singleton',
|
|
23
|
+
factory: () => defaultSecurityPolicy(),
|
|
24
|
+
});
|
|
35
25
|
//# sourceMappingURL=security-policy.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"security-policy.js","sourceRoot":"","sources":["../src/security-policy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"security-policy.js","sourceRoot":"","sources":["../src/security-policy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAc,MAAM,mBAAmB,CAAA;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAA;AAyClE;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,GAAmB,EAAE,CAAC,CAAC;IAC1D,MAAM,EAAE,oBAAoB;IAC5B,uBAAuB,EAAE,EAAE;IAC3B,sBAAsB,EAAE,CAAC;IACzB,mBAAmB,EAAE,OAAO;IAC5B,2BAA2B,EAAE,EAAE;CAChC,CAAC,CAAA;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAuC,aAAa,CAAC;IAC9E,IAAI,EAAE,mCAAmC;IACzC,QAAQ,EAAE,WAAW;IACrB,OAAO,EAAE,GAAG,EAAE,CAAC,qBAAqB,EAAE;CACvC,CAAC,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@furystack/security",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.1",
|
|
4
4
|
"description": "Security, password management and authorization related stuff for FuryStack",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -37,16 +37,16 @@
|
|
|
37
37
|
"url": "https://github.com/furystack/furystack/issues"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@furystack/core": "^
|
|
41
|
-
"@furystack/inject": "^
|
|
42
|
-
"@furystack/repository": "^
|
|
40
|
+
"@furystack/core": "^17.1.0",
|
|
41
|
+
"@furystack/inject": "^13.0.1",
|
|
42
|
+
"@furystack/repository": "^11.0.1"
|
|
43
43
|
},
|
|
44
44
|
"homepage": "https://github.com/furystack/furystack",
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@furystack/utils": "^
|
|
47
|
-
"@types/node": "^25.
|
|
46
|
+
"@furystack/utils": "^9.0.1",
|
|
47
|
+
"@types/node": "^25.9.1",
|
|
48
48
|
"typescript": "^6.0.3",
|
|
49
|
-
"vitest": "^4.1.
|
|
49
|
+
"vitest": "^4.1.7"
|
|
50
50
|
},
|
|
51
51
|
"engines": {
|
|
52
52
|
"node": ">=22.0.0"
|
|
@@ -1,44 +1,51 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineService, type Token } from '@furystack/inject'
|
|
2
|
+
import { pbkdf2, randomBytes } from 'crypto'
|
|
3
|
+
import type { PasswordCheckResult } from './models/index.js'
|
|
2
4
|
import type { PasswordHasher } from './password-hasher.js'
|
|
3
|
-
import { randomBytes, pbkdf2 } from 'crypto'
|
|
4
|
-
import type { PasswordCheckResult, PasswordCredential } from './models/index.js'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
private readonly keylen = 64
|
|
6
|
+
const ITERATIONS = 1000
|
|
7
|
+
const KEY_LENGTH = 64
|
|
8
|
+
const SALT_BYTES = 16
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
)
|
|
19
|
-
)
|
|
20
|
-
|
|
10
|
+
const deriveHash = (password: string, salt: string): Promise<string> =>
|
|
11
|
+
new Promise<string>((resolve, reject) => {
|
|
12
|
+
pbkdf2(password, salt, ITERATIONS, KEY_LENGTH, 'sha512', (err, key) => {
|
|
13
|
+
if (err) {
|
|
14
|
+
reject(err)
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
resolve(key.toString('hex'))
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Default PBKDF2-based {@link PasswordHasher} implementation. Uses SHA-512
|
|
23
|
+
* with 1000 iterations and a 64-byte derived key length.
|
|
24
|
+
*
|
|
25
|
+
* Swap this token in {@link SecurityPolicy.hasher} to replace the hashing
|
|
26
|
+
* algorithm — for example with a stronger Argon2 implementation or a
|
|
27
|
+
* delegated hashing service.
|
|
28
|
+
*/
|
|
29
|
+
export const CryptoPasswordHasher: Token<PasswordHasher, 'singleton'> = defineService({
|
|
30
|
+
name: 'furystack/security/CryptoPasswordHasher',
|
|
31
|
+
lifetime: 'singleton',
|
|
32
|
+
factory: (): PasswordHasher => ({
|
|
33
|
+
verifyCredential: async (password, { passwordHash, salt }): Promise<PasswordCheckResult> => {
|
|
34
|
+
const candidate = await deriveHash(password, salt)
|
|
35
|
+
if (candidate === passwordHash) {
|
|
36
|
+
return { isValid: true }
|
|
37
|
+
}
|
|
38
|
+
return { isValid: false, reason: 'badUsernameOrPassword' }
|
|
39
|
+
},
|
|
40
|
+
createCredential: async (userName, password) => {
|
|
41
|
+
const salt = randomBytes(SALT_BYTES).toString('hex')
|
|
42
|
+
const passwordHash = await deriveHash(password, salt)
|
|
21
43
|
return {
|
|
22
|
-
|
|
44
|
+
userName,
|
|
45
|
+
salt,
|
|
46
|
+
passwordHash,
|
|
47
|
+
creationDate: new Date().toISOString(),
|
|
23
48
|
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
reason: 'badUsernameOrPassword',
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
public async createCredential(userName: string, password: string) {
|
|
31
|
-
const salt = randomBytes(16).toString('hex')
|
|
32
|
-
const passwordHash = await new Promise<string>((resolve, reject) =>
|
|
33
|
-
pbkdf2(password, salt, this.iterations, this.keylen, 'sha512', (err, key) =>
|
|
34
|
-
err ? reject(err) : resolve(key.toString('hex')),
|
|
35
|
-
),
|
|
36
|
-
)
|
|
37
|
-
return {
|
|
38
|
-
userName,
|
|
39
|
-
salt,
|
|
40
|
-
passwordHash,
|
|
41
|
-
creationDate: new Date().toISOString(),
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
})
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import type { PasswordComplexityRule } from '../models/password-complexity-rule.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Thrown by password-setting flows when the candidate password fails one or
|
|
5
|
+
* more configured {@link PasswordComplexityRule}s. `result` carries the
|
|
6
|
+
* per-rule failures so callers can render targeted user feedback.
|
|
7
|
+
*/
|
|
1
8
|
export class PasswordComplexityError extends Error {
|
|
2
|
-
/**
|
|
3
|
-
* @param result A detailed error result array
|
|
4
|
-
* @param message The Error message
|
|
5
|
-
*/
|
|
6
9
|
constructor(
|
|
7
10
|
public readonly result: Array<{ message: string; rule: string }>,
|
|
8
11
|
message?: string,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Thrown when credential verification fails: bad username/password, expired
|
|
3
|
+
* password, or expired/invalid reset token. REST handlers should map this
|
|
4
|
+
* to a 401 response.
|
|
3
5
|
*/
|
|
4
6
|
export class UnauthenticatedError extends Error {}
|
package/src/helpers.ts
CHANGED
|
@@ -1,22 +1,34 @@
|
|
|
1
1
|
import type { Injector } from '@furystack/inject'
|
|
2
|
-
import {
|
|
2
|
+
import { PasswordAuthenticator } from './password-authenticator.js'
|
|
3
|
+
import { SecurityPolicyManager } from './security-policy-manager.js'
|
|
4
|
+
import { defaultSecurityPolicy, SecurityPolicy } from './security-policy.js'
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
|
-
*
|
|
7
|
+
* Rebinds the {@link SecurityPolicy} on the given injector with the default
|
|
8
|
+
* policy merged with `overrides`.
|
|
6
9
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
+
* Apply this at application setup before resolving any service that reads
|
|
11
|
+
* the policy — {@link PasswordAuthenticator}, {@link SecurityPolicyManager}
|
|
12
|
+
* or direct `injector.get(SecurityPolicy)` callers. Because those services
|
|
13
|
+
* capture the policy at resolve time, calling `usePasswordPolicy` after
|
|
14
|
+
* they have been resolved has no effect unless you also invalidate them.
|
|
10
15
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
16
|
+
* **Prerequisite:** bind persistent implementations of
|
|
17
|
+
* `PasswordCredentialStore` and `PasswordResetTokenStore` before resolving
|
|
18
|
+
* {@link PasswordAuthenticator}. Their default factories throw on purpose
|
|
19
|
+
* so password data is never silently kept in-memory.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* usePasswordPolicy(injector, {
|
|
24
|
+
* passwordExpirationDays: 90,
|
|
25
|
+
* passwordComplexityRules: [createMinLengthComplexityRule(8)],
|
|
26
|
+
* })
|
|
27
|
+
* ```
|
|
13
28
|
*/
|
|
14
|
-
export const usePasswordPolicy = (injector: Injector,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
injector.setExplicitInstance(plainPolicy, SecurityPolicy)
|
|
29
|
+
export const usePasswordPolicy = (injector: Injector, overrides?: Partial<SecurityPolicy>): void => {
|
|
30
|
+
injector.bind(SecurityPolicy, () => ({ ...defaultSecurityPolicy(), ...overrides }))
|
|
31
|
+
injector.invalidate(SecurityPolicy)
|
|
32
|
+
injector.invalidate(SecurityPolicyManager)
|
|
33
|
+
injector.invalidate(PasswordAuthenticator)
|
|
22
34
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
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
|
-
|
|
9
|
-
export * from './helpers.js'
|
|
10
|
+
export * from './security-policy.js'
|
|
@@ -1,23 +1,13 @@
|
|
|
1
|
+
import type { PasswordHasher } from '../password-hasher.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
4
|
+
* Persisted password credential. `passwordHash` and `salt` are produced by
|
|
5
|
+
* the configured {@link PasswordHasher}; `creationDate` (ISO 8601 string)
|
|
6
|
+
* is the source of truth for password-expiration policies.
|
|
3
7
|
*/
|
|
4
8
|
export class PasswordCredential {
|
|
5
|
-
/**
|
|
6
|
-
* The unique name of the user
|
|
7
|
-
*/
|
|
8
9
|
declare userName: string
|
|
9
|
-
/**
|
|
10
|
-
* The hashed password value
|
|
11
|
-
*/
|
|
12
10
|
declare passwordHash: string
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Salt value for password hashing // TODO
|
|
16
|
-
*/
|
|
17
11
|
declare salt: string
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* The Creation date in ISO String format
|
|
21
|
-
*/
|
|
22
12
|
declare creationDate: string
|
|
23
13
|
}
|
|
@@ -1,14 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-use password reset token. `token` is the URL-safe identifier
|
|
3
|
+
* shared with the user (e.g. via email). `createdAt` (ISO 8601) is the
|
|
4
|
+
* expiration anchor honoured by `SecurityPolicyManager.hasTokenExpired`.
|
|
5
|
+
*/
|
|
1
6
|
export class PasswordResetToken {
|
|
2
|
-
/**
|
|
3
|
-
* The related user name
|
|
4
|
-
*/
|
|
5
7
|
declare userName: string
|
|
6
|
-
/**
|
|
7
|
-
* The Token value - should be some kind of generated UUID that can be included e.g. in URLs
|
|
8
|
-
*/
|
|
9
8
|
declare token: string
|
|
10
|
-
/**
|
|
11
|
-
* The creation date in an ISO datetime format
|
|
12
|
-
*/
|
|
13
9
|
declare createdAt: string
|
|
14
10
|
}
|