@furystack/security 1.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.
Files changed (139) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +339 -0
  3. package/README.md +4 -0
  4. package/dist/crypto-password-hasher.d.ts +14 -0
  5. package/dist/crypto-password-hasher.d.ts.map +1 -0
  6. package/dist/crypto-password-hasher.js +39 -0
  7. package/dist/crypto-password-hasher.js.map +1 -0
  8. package/dist/errors/index.d.ts +4 -0
  9. package/dist/errors/index.d.ts.map +1 -0
  10. package/dist/errors/index.js +7 -0
  11. package/dist/errors/index.js.map +1 -0
  12. package/dist/errors/password-complexity-error.d.ts +15 -0
  13. package/dist/errors/password-complexity-error.d.ts.map +1 -0
  14. package/dist/errors/password-complexity-error.js +15 -0
  15. package/dist/errors/password-complexity-error.js.map +1 -0
  16. package/dist/errors/unauthenticated-error.d.ts +6 -0
  17. package/dist/errors/unauthenticated-error.d.ts.map +1 -0
  18. package/dist/errors/unauthenticated-error.js +10 -0
  19. package/dist/errors/unauthenticated-error.js.map +1 -0
  20. package/dist/index.d.ts +9 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +12 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/injector-extensions.d.ts +13 -0
  25. package/dist/injector-extensions.d.ts.map +1 -0
  26. package/dist/injector-extensions.js +11 -0
  27. package/dist/injector-extensions.js.map +1 -0
  28. package/dist/models/index.d.ts +6 -0
  29. package/dist/models/index.d.ts.map +1 -0
  30. package/dist/models/index.js +9 -0
  31. package/dist/models/index.js.map +1 -0
  32. package/dist/models/password-check-result.d.ts +10 -0
  33. package/dist/models/password-check-result.d.ts.map +1 -0
  34. package/dist/models/password-check-result.js +3 -0
  35. package/dist/models/password-check-result.js.map +1 -0
  36. package/dist/models/password-complexity-result.d.ts +10 -0
  37. package/dist/models/password-complexity-result.d.ts.map +1 -0
  38. package/dist/models/password-complexity-result.js +3 -0
  39. package/dist/models/password-complexity-result.js.map +1 -0
  40. package/dist/models/password-complexity-rule.d.ts +6 -0
  41. package/dist/models/password-complexity-rule.d.ts.map +1 -0
  42. package/dist/models/password-complexity-rule.js +3 -0
  43. package/dist/models/password-complexity-rule.js.map +1 -0
  44. package/dist/models/password-credential.d.ts +22 -0
  45. package/dist/models/password-credential.d.ts.map +1 -0
  46. package/dist/models/password-credential.js +10 -0
  47. package/dist/models/password-credential.js.map +1 -0
  48. package/dist/models/password-reset-token.d.ts +15 -0
  49. package/dist/models/password-reset-token.d.ts.map +1 -0
  50. package/dist/models/password-reset-token.js +7 -0
  51. package/dist/models/password-reset-token.js.map +1 -0
  52. package/dist/password-authenticator.d.ts +38 -0
  53. package/dist/password-authenticator.d.ts.map +1 -0
  54. package/dist/password-authenticator.js +99 -0
  55. package/dist/password-authenticator.js.map +1 -0
  56. package/dist/password-authenticator.spec.d.ts +2 -0
  57. package/dist/password-authenticator.spec.d.ts.map +1 -0
  58. package/dist/password-authenticator.spec.js +309 -0
  59. package/dist/password-authenticator.spec.js.map +1 -0
  60. package/dist/password-complexity-rules/contains-lowercase.d.ts +7 -0
  61. package/dist/password-complexity-rules/contains-lowercase.d.ts.map +1 -0
  62. package/dist/password-complexity-rules/contains-lowercase.js +29 -0
  63. package/dist/password-complexity-rules/contains-lowercase.js.map +1 -0
  64. package/dist/password-complexity-rules/contains-lowercase.spec.d.ts +2 -0
  65. package/dist/password-complexity-rules/contains-lowercase.spec.d.ts.map +1 -0
  66. package/dist/password-complexity-rules/contains-lowercase.spec.js +28 -0
  67. package/dist/password-complexity-rules/contains-lowercase.spec.js.map +1 -0
  68. package/dist/password-complexity-rules/contains-uppercase.d.ts +7 -0
  69. package/dist/password-complexity-rules/contains-uppercase.d.ts.map +1 -0
  70. package/dist/password-complexity-rules/contains-uppercase.js +27 -0
  71. package/dist/password-complexity-rules/contains-uppercase.js.map +1 -0
  72. package/dist/password-complexity-rules/contains-uppercase.spec.d.ts +2 -0
  73. package/dist/password-complexity-rules/contains-uppercase.spec.d.ts.map +1 -0
  74. package/dist/password-complexity-rules/contains-uppercase.spec.js +28 -0
  75. package/dist/password-complexity-rules/contains-uppercase.spec.js.map +1 -0
  76. package/dist/password-complexity-rules/index.d.ts +5 -0
  77. package/dist/password-complexity-rules/index.d.ts.map +1 -0
  78. package/dist/password-complexity-rules/index.js +8 -0
  79. package/dist/password-complexity-rules/index.js.map +1 -0
  80. package/dist/password-complexity-rules/max-length.d.ts +7 -0
  81. package/dist/password-complexity-rules/max-length.d.ts.map +1 -0
  82. package/dist/password-complexity-rules/max-length.js +19 -0
  83. package/dist/password-complexity-rules/max-length.js.map +1 -0
  84. package/dist/password-complexity-rules/max-length.spec.d.ts +2 -0
  85. package/dist/password-complexity-rules/max-length.spec.d.ts.map +1 -0
  86. package/dist/password-complexity-rules/max-length.spec.js +17 -0
  87. package/dist/password-complexity-rules/max-length.spec.js.map +1 -0
  88. package/dist/password-complexity-rules/min-length.d.ts +7 -0
  89. package/dist/password-complexity-rules/min-length.d.ts.map +1 -0
  90. package/dist/password-complexity-rules/min-length.js +19 -0
  91. package/dist/password-complexity-rules/min-length.js.map +1 -0
  92. package/dist/password-complexity-rules/min-length.spec.d.ts +2 -0
  93. package/dist/password-complexity-rules/min-length.spec.d.ts.map +1 -0
  94. package/dist/password-complexity-rules/min-length.spec.js +17 -0
  95. package/dist/password-complexity-rules/min-length.spec.js.map +1 -0
  96. package/dist/password-hasher.d.ts +6 -0
  97. package/dist/password-hasher.d.ts.map +1 -0
  98. package/dist/password-hasher.js +18 -0
  99. package/dist/password-hasher.js.map +1 -0
  100. package/dist/security-policy-manager.d.ts +26 -0
  101. package/dist/security-policy-manager.d.ts.map +1 -0
  102. package/dist/security-policy-manager.js +55 -0
  103. package/dist/security-policy-manager.js.map +1 -0
  104. package/dist/security-policy-manager.spec.d.ts +2 -0
  105. package/dist/security-policy-manager.spec.d.ts.map +1 -0
  106. package/dist/security-policy-manager.spec.js +19 -0
  107. package/dist/security-policy-manager.spec.js.map +1 -0
  108. package/dist/security-policy.d.ts +26 -0
  109. package/dist/security-policy.d.ts.map +1 -0
  110. package/dist/security-policy.js +35 -0
  111. package/dist/security-policy.js.map +1 -0
  112. package/package.json +45 -0
  113. package/src/crypto-password-hasher.ts +44 -0
  114. package/src/errors/index.ts +3 -0
  115. package/src/errors/password-complexity-error.ts +9 -0
  116. package/src/errors/unauthenticated-error.ts +4 -0
  117. package/src/index.ts +9 -0
  118. package/src/injector-extensions.ts +21 -0
  119. package/src/models/index.ts +5 -0
  120. package/src/models/password-check-result.ts +11 -0
  121. package/src/models/password-complexity-result.ts +11 -0
  122. package/src/models/password-complexity-rule.ts +6 -0
  123. package/src/models/password-credential.ts +23 -0
  124. package/src/models/password-reset-token.ts +14 -0
  125. package/src/password-authenticator.spec.ts +393 -0
  126. package/src/password-authenticator.ts +101 -0
  127. package/src/password-complexity-rules/contains-lowercase.spec.ts +34 -0
  128. package/src/password-complexity-rules/contains-lowercase.ts +27 -0
  129. package/src/password-complexity-rules/contains-uppercase.spec.ts +34 -0
  130. package/src/password-complexity-rules/contains-uppercase.ts +24 -0
  131. package/src/password-complexity-rules/index.ts +4 -0
  132. package/src/password-complexity-rules/max-length.spec.ts +17 -0
  133. package/src/password-complexity-rules/max-length.ts +16 -0
  134. package/src/password-complexity-rules/min-length.spec.ts +19 -0
  135. package/src/password-complexity-rules/min-length.ts +16 -0
  136. package/src/password-hasher.ts +13 -0
  137. package/src/security-policy-manager.spec.ts +16 -0
  138. package/src/security-policy-manager.ts +53 -0
  139. package/src/security-policy.ts +32 -0
@@ -0,0 +1,34 @@
1
+ import { PasswordComplexityFailedResult } from '../models'
2
+ import { createContainsLowercasePolicy } from './contains-lowercase'
3
+
4
+ describe('Contains-lowercase', () => {
5
+ it('Should fail if the password is shorter than expected', async () => {
6
+ const policy = createContainsLowercasePolicy(3)
7
+ const result = await policy.check('')
8
+ expect(result.success).toBe(false)
9
+ expect((result as PasswordComplexityFailedResult).message).toBe(
10
+ 'The password should contain at least 3 lower case characters',
11
+ )
12
+ })
13
+
14
+ it('Should fail if the password does not contain enough lowercase chars', async () => {
15
+ const policy = createContainsLowercasePolicy(3)
16
+ const result = await policy.check('ASD12345(=/!+')
17
+ expect(result.success).toBe(false)
18
+ expect((result as PasswordComplexityFailedResult).message).toBe(
19
+ 'The password should contain at least 3 lower case characters',
20
+ )
21
+ })
22
+
23
+ it('Should succeed if the password contains enough lowercase chars', async () => {
24
+ const policy = createContainsLowercasePolicy(3)
25
+ const result = await policy.check('asd')
26
+ expect(result.success).toBe(true)
27
+ })
28
+
29
+ it('Should succeed if the password contains enough lowercase chars supporting special ones', async () => {
30
+ const policy = createContainsLowercasePolicy(3)
31
+ const result = await policy.check('éáő')
32
+ expect(result.success).toBe(true)
33
+ })
34
+ })
@@ -0,0 +1,27 @@
1
+ import { PasswordComplexityRule } from '../models'
2
+
3
+ /**
4
+ * @param lowerCaseCount The minimum lower case characters length
5
+ * @returns The created Password Policy object
6
+ */
7
+ export const createContainsLowercasePolicy = (lowerCaseCount: number): PasswordComplexityRule => ({
8
+ name: 'containsLowercase' as const,
9
+ check: async (passwordString: string) => {
10
+ const actualLowerCaseCount =
11
+ passwordString.length >= lowerCaseCount
12
+ ? passwordString
13
+ .normalize('NFD')
14
+ .replace(/\p{Diacritic}/gu, '')
15
+ .replace(/[^a-z]/g, '').length
16
+ : 0
17
+ const success = actualLowerCaseCount >= lowerCaseCount
18
+ if (success) {
19
+ return { success }
20
+ }
21
+ return {
22
+ success,
23
+ message: `The password should contain at least ${lowerCaseCount} lower case characters`,
24
+ rule: 'containsLowercase',
25
+ }
26
+ },
27
+ })
@@ -0,0 +1,34 @@
1
+ import { PasswordComplexityFailedResult } from '../models'
2
+ import { createContainsUppercasePolicy } from './contains-uppercase'
3
+
4
+ describe('Contains-uppercase', () => {
5
+ it('Should fail if the password is shorter than expected', async () => {
6
+ const policy = createContainsUppercasePolicy(3)
7
+ const result = await policy.check('')
8
+ expect(result.success).toBe(false)
9
+ expect((result as PasswordComplexityFailedResult).message).toBe(
10
+ 'The password should contain at least 3 upper case characters',
11
+ )
12
+ })
13
+
14
+ it('Should fail if the password does not contain enough lowercase chars', async () => {
15
+ const policy = createContainsUppercasePolicy(3)
16
+ const result = await policy.check('asd12345(=/!+')
17
+ expect(result.success).toBe(false)
18
+ expect((result as PasswordComplexityFailedResult).message).toBe(
19
+ 'The password should contain at least 3 upper case characters',
20
+ )
21
+ })
22
+
23
+ it('Should succeed if the password contains enough lowercase chars', async () => {
24
+ const policy = createContainsUppercasePolicy(3)
25
+ const result = await policy.check('ASD')
26
+ expect(result.success).toBe(true)
27
+ })
28
+
29
+ it('Should succeed if the password contains enough lowercase chars supporting special ones', async () => {
30
+ const policy = createContainsUppercasePolicy(3)
31
+ const result = await policy.check('ÉÁÍ')
32
+ expect(result.success).toBe(true)
33
+ })
34
+ })
@@ -0,0 +1,24 @@
1
+ import { PasswordComplexityRule } from '../models'
2
+
3
+ /**
4
+ * @param upperCaseCount The minimum upper case characters length
5
+ * @returns The created Password Policy object
6
+ */
7
+ export const createContainsUppercasePolicy = (upperCaseCount: number): PasswordComplexityRule => ({
8
+ name: 'containsUppercase' as const,
9
+ check: async (passwordString: string) => {
10
+ const actualUpperCaseCount = passwordString
11
+ .normalize('NFD')
12
+ .replace(/\p{Diacritic}/gu, '')
13
+ .replace(/[^A-Z]/g, '').length
14
+ const success = actualUpperCaseCount >= upperCaseCount
15
+ if (success) {
16
+ return { success }
17
+ }
18
+ return {
19
+ success,
20
+ message: `The password should contain at least ${upperCaseCount} upper case characters`,
21
+ rule: 'containsUppercase',
22
+ }
23
+ },
24
+ })
@@ -0,0 +1,4 @@
1
+ export * from './contains-lowercase'
2
+ export * from './contains-uppercase'
3
+ export * from './max-length'
4
+ export * from './min-length'
@@ -0,0 +1,17 @@
1
+ import { PasswordComplexityFailedResult } from '../models'
2
+ import { createMaxLengthComplexityRule } from './max-length'
3
+
4
+ describe('max-length', () => {
5
+ it('Should fail if the password is longer than expected', async () => {
6
+ const policy = createMaxLengthComplexityRule(3)
7
+ const result = await policy.check('asdasd123')
8
+ expect(result.success).toBe(false)
9
+ expect((result as PasswordComplexityFailedResult).message).toBe('The password has to be maximum 3 character length')
10
+ })
11
+
12
+ it('Should fail if the password does not contain enough lowercase chars', async () => {
13
+ const policy = createMaxLengthComplexityRule(3)
14
+ const result = await policy.check('asd')
15
+ expect(result.success).toBe(true)
16
+ })
17
+ })
@@ -0,0 +1,16 @@
1
+ import type { PasswordComplexityRule } from '../models'
2
+
3
+ /**
4
+ * @param maxLength The maximum password length
5
+ * @returns The created Password Policy object
6
+ */
7
+ export const createMaxLengthComplexityRule = (maxLength: number): PasswordComplexityRule => ({
8
+ name: 'maxLength' as const,
9
+ check: async (passwordString: string) => {
10
+ const success = passwordString.length <= maxLength
11
+ if (success) {
12
+ return { success }
13
+ }
14
+ return { success, message: `The password has to be maximum ${maxLength} character length`, rule: 'maxLength' }
15
+ },
16
+ })
@@ -0,0 +1,19 @@
1
+ import { PasswordComplexityFailedResult } from '../models'
2
+ import { createMinLengthComplexityRule } from './min-length'
3
+
4
+ describe('min-length', () => {
5
+ it('Should fail if the password is shorter than expected', async () => {
6
+ const policy = createMinLengthComplexityRule(3)
7
+ const result = await policy.check('')
8
+ expect(result.success).toBe(false)
9
+ expect((result as PasswordComplexityFailedResult).message).toBe(
10
+ 'The password has to be at least 3 character length',
11
+ )
12
+ })
13
+
14
+ it('Should fail if the password does not contain enough lowercase chars', async () => {
15
+ const policy = createMinLengthComplexityRule(3)
16
+ const result = await policy.check('asd12345(=/!+')
17
+ expect(result.success).toBe(true)
18
+ })
19
+ })
@@ -0,0 +1,16 @@
1
+ import type { PasswordComplexityRule } from '../models'
2
+
3
+ /**
4
+ * @param minLength The minimum password length
5
+ * @returns The created Password Policy object
6
+ */
7
+ export const createMinLengthComplexityRule = (minLength: number): PasswordComplexityRule => ({
8
+ name: 'minLength' as const,
9
+ check: async (passwordString: string) => {
10
+ const success = passwordString.length >= minLength
11
+ if (success) {
12
+ return { success }
13
+ }
14
+ return { success, message: `The password has to be at least ${minLength} character length`, rule: 'minLength' }
15
+ },
16
+ })
@@ -0,0 +1,13 @@
1
+ import { Injectable } from '@furystack/inject'
2
+ import { PasswordCheckResult, PasswordCredential } from './models'
3
+
4
+ @Injectable({ lifetime: 'singleton' })
5
+ export class PasswordHasher {
6
+ public createCredential(_userName: string, _password: string): Promise<PasswordCredential> {
7
+ throw Error('Set up hasher - TODO')
8
+ }
9
+
10
+ public verifyCredential(_password: string, _credential: PasswordCredential): Promise<PasswordCheckResult> {
11
+ throw Error('Set up hasher - TODO')
12
+ }
13
+ }
@@ -0,0 +1,16 @@
1
+ import { Injector } from '@furystack/inject'
2
+ import { using } from '@furystack/utils'
3
+ import { v4 } from 'uuid'
4
+ import './injector-extensions'
5
+ import { SecurityPolicyManager } from './security-policy-manager'
6
+ describe('SecurityPolicyManager', () => {
7
+ it('Should return false when no token expiration has been set', () => {
8
+ using(new Injector(), (i) => {
9
+ i.usePasswordPolicy({ resetTokenExpirationSeconds: 0 })
10
+ const result = i
11
+ .getInstance(SecurityPolicyManager)
12
+ .hasTokenExpired({ createdAt: new Date().toISOString(), token: v4(), userName: v4() })
13
+ expect(result).toBeFalsy()
14
+ })
15
+ })
16
+ })
@@ -0,0 +1,53 @@
1
+ import { Injectable } from '@furystack/inject'
2
+ import { PasswordCredential, PasswordResetToken } from './models'
3
+ import { SecurityPolicy } from './security-policy'
4
+
5
+ @Injectable({ lifetime: 'singleton' })
6
+ export class SecurityPolicyManager {
7
+ /**
8
+ * @param password The Plain password string
9
+ * @returns if the password matches the complexity rules
10
+ */
11
+ public async matchPasswordComplexityRules(password: string) {
12
+ const result = await Promise.all(this.policy.passwordComplexityRules.map((rule) => rule.check(password)))
13
+ const failed = result.filter((r) => r.success === false)
14
+ if (failed.length) {
15
+ return {
16
+ match: false as const,
17
+ errors: failed.map((f) => f.success === false && { message: f.message, rule: f.rule }) as Array<{
18
+ message: string
19
+ rule: string
20
+ }>,
21
+ }
22
+ }
23
+
24
+ return {
25
+ match: true as const,
26
+ }
27
+ }
28
+
29
+ public hasPasswordExpired(credential: PasswordCredential) {
30
+ if (!this.policy.passwordExpirationDays) {
31
+ return false
32
+ }
33
+ const credentialExpiration = new Date(credential.creationDate)
34
+ credentialExpiration.setDate(credentialExpiration.getDate() + this.policy.passwordExpirationDays)
35
+ const now = new Date()
36
+ return now > credentialExpiration
37
+ }
38
+
39
+ public hasTokenExpired(token: PasswordResetToken) {
40
+ if (!this.policy.resetTokenExpirationSeconds) {
41
+ return false
42
+ }
43
+ const tokenExpiration = new Date(token.createdAt)
44
+ tokenExpiration.setSeconds(tokenExpiration.getSeconds() + this.policy.resetTokenExpirationSeconds)
45
+ const now = new Date()
46
+ return now > tokenExpiration
47
+ }
48
+
49
+ /**
50
+ * @param policy The related Password Policy object
51
+ */
52
+ constructor(public readonly policy: SecurityPolicy) {}
53
+ }
@@ -0,0 +1,32 @@
1
+ import { Constructable, Injectable } from '@furystack/inject'
2
+ import { CryptoPasswordHasher } from './crypto-password-hasher'
3
+ import { PasswordComplexityRule } from './models'
4
+ import { PasswordHasher } from './password-hasher'
5
+
6
+ @Injectable({ lifetime: 'singleton' })
7
+ export class SecurityPolicy {
8
+ /**
9
+ * A Hasher instance for password hash generation
10
+ */
11
+ hasher: Constructable<PasswordHasher> = CryptoPasswordHasher
12
+
13
+ /**
14
+ * Array of password complexity rules to apply
15
+ */
16
+ passwordComplexityRules: PasswordComplexityRule[] = []
17
+
18
+ /**
19
+ * The password expiration in days
20
+ */
21
+ passwordExpirationDays = 0
22
+
23
+ /**
24
+ * Defines if the login needs a second factor authentication
25
+ */
26
+ requireSecondFactor: 'never' | 'always' = 'never' // TODO - Challenge based on trusted devices
27
+
28
+ /**
29
+ * The Password Reset Token expiration in seconds
30
+ */
31
+ resetTokenExpirationSeconds = 15
32
+ }