@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.
Files changed (90) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +9 -3
  3. package/esm/crypto-password-hasher.d.ts +10 -12
  4. package/esm/crypto-password-hasher.d.ts.map +1 -1
  5. package/esm/crypto-password-hasher.js +43 -36
  6. package/esm/crypto-password-hasher.js.map +1 -1
  7. package/esm/errors/password-complexity-error.d.ts +5 -4
  8. package/esm/errors/password-complexity-error.d.ts.map +1 -1
  9. package/esm/errors/password-complexity-error.js +5 -4
  10. package/esm/errors/password-complexity-error.js.map +1 -1
  11. package/esm/errors/unauthenticated-error.d.ts +3 -1
  12. package/esm/errors/unauthenticated-error.d.ts.map +1 -1
  13. package/esm/errors/unauthenticated-error.js +3 -1
  14. package/esm/errors/unauthenticated-error.js.map +1 -1
  15. package/esm/helpers.d.ts +20 -7
  16. package/esm/helpers.d.ts.map +1 -1
  17. package/esm/helpers.js +27 -13
  18. package/esm/helpers.js.map +1 -1
  19. package/esm/index.d.ts +3 -1
  20. package/esm/index.d.ts.map +1 -1
  21. package/esm/index.js +3 -1
  22. package/esm/index.js.map +1 -1
  23. package/esm/models/password-credential.d.ts +3 -13
  24. package/esm/models/password-credential.d.ts.map +1 -1
  25. package/esm/models/password-credential.js +3 -1
  26. package/esm/models/password-credential.js.map +1 -1
  27. package/esm/models/password-reset-token.d.ts +5 -9
  28. package/esm/models/password-reset-token.d.ts.map +1 -1
  29. package/esm/models/password-reset-token.js +5 -0
  30. package/esm/models/password-reset-token.js.map +1 -1
  31. package/esm/password-authenticator.d.ts +26 -15
  32. package/esm/password-authenticator.d.ts.map +1 -1
  33. package/esm/password-authenticator.js +73 -109
  34. package/esm/password-authenticator.js.map +1 -1
  35. package/esm/password-authenticator.spec.js +122 -212
  36. package/esm/password-authenticator.spec.js.map +1 -1
  37. package/esm/password-complexity-rules/contains-lowercase.d.ts +2 -2
  38. package/esm/password-complexity-rules/contains-lowercase.js +2 -2
  39. package/esm/password-complexity-rules/contains-uppercase.d.ts +2 -2
  40. package/esm/password-complexity-rules/contains-uppercase.js +2 -2
  41. package/esm/password-complexity-rules/max-length.d.ts +1 -4
  42. package/esm/password-complexity-rules/max-length.d.ts.map +1 -1
  43. package/esm/password-complexity-rules/max-length.js +1 -4
  44. package/esm/password-complexity-rules/max-length.js.map +1 -1
  45. package/esm/password-complexity-rules/min-length.d.ts +1 -4
  46. package/esm/password-complexity-rules/min-length.d.ts.map +1 -1
  47. package/esm/password-complexity-rules/min-length.js +1 -4
  48. package/esm/password-complexity-rules/min-length.js.map +1 -1
  49. package/esm/password-credential-store.d.ts +50 -0
  50. package/esm/password-credential-store.d.ts.map +1 -0
  51. package/esm/password-credential-store.js +73 -0
  52. package/esm/password-credential-store.js.map +1 -0
  53. package/esm/password-credential-store.spec.d.ts +2 -0
  54. package/esm/password-credential-store.spec.d.ts.map +1 -0
  55. package/esm/password-credential-store.spec.js +64 -0
  56. package/esm/password-credential-store.spec.js.map +1 -0
  57. package/esm/password-hasher.d.ts +21 -3
  58. package/esm/password-hasher.d.ts.map +1 -1
  59. package/esm/password-hasher.js +1 -19
  60. package/esm/password-hasher.js.map +1 -1
  61. package/esm/security-policy-manager.d.ts +46 -14
  62. package/esm/security-policy-manager.d.ts.map +1 -1
  63. package/esm/security-policy-manager.js +48 -52
  64. package/esm/security-policy-manager.js.map +1 -1
  65. package/esm/security-policy-manager.spec.js +58 -7
  66. package/esm/security-policy-manager.spec.js.map +1 -1
  67. package/esm/security-policy.d.ts +35 -9
  68. package/esm/security-policy.d.ts.map +1 -1
  69. package/esm/security-policy.js +23 -33
  70. package/esm/security-policy.js.map +1 -1
  71. package/package.json +7 -7
  72. package/src/crypto-password-hasher.ts +46 -39
  73. package/src/errors/password-complexity-error.ts +7 -4
  74. package/src/errors/unauthenticated-error.ts +3 -1
  75. package/src/helpers.ts +27 -15
  76. package/src/index.ts +3 -2
  77. package/src/models/password-credential.ts +5 -15
  78. package/src/models/password-reset-token.ts +5 -9
  79. package/src/password-authenticator.spec.ts +134 -254
  80. package/src/password-authenticator.ts +92 -86
  81. package/src/password-complexity-rules/contains-lowercase.ts +2 -2
  82. package/src/password-complexity-rules/contains-uppercase.ts +2 -2
  83. package/src/password-complexity-rules/max-length.ts +1 -4
  84. package/src/password-complexity-rules/min-length.ts +1 -4
  85. package/src/password-credential-store.spec.ts +77 -0
  86. package/src/password-credential-store.ts +79 -0
  87. package/src/password-hasher.ts +21 -9
  88. package/src/security-policy-manager.spec.ts +63 -7
  89. package/src/security-policy-manager.ts +91 -42
  90. package/src/security-policy.ts +50 -18
@@ -1,6 +1,5 @@
1
- import { addStore, InMemoryStore, StoreManager, User } from '@furystack/core'
2
- import { Injector } from '@furystack/inject'
3
- import { getRepository } from '@furystack/repository'
1
+ import { InMemoryStore } from '@furystack/core'
2
+ import { createInjector, type Injector } from '@furystack/inject'
4
3
  import { usingAsync } from '@furystack/utils'
5
4
  import { randomBytes } from 'crypto'
6
5
  import { describe, expect, it, vi } from 'vitest'
@@ -11,353 +10,234 @@ import type { PasswordCheckFailedResult } from './models/password-check-result.j
11
10
  import { PasswordCredential } from './models/password-credential.js'
12
11
  import { PasswordResetToken } from './models/password-reset-token.js'
13
12
  import { PasswordAuthenticator } from './password-authenticator.js'
13
+ import { PasswordCredentialStore, PasswordResetTokenStore } from './password-credential-store.js'
14
14
  import { createMinLengthComplexityRule } from './password-complexity-rules/min-length.js'
15
15
  import { SecurityPolicyManager } from './security-policy-manager.js'
16
16
 
17
17
  const randomString = () => randomBytes(32).toString('hex')
18
18
 
19
- const setupSecurityStores = (i: Injector) => {
20
- addStore(i, new InMemoryStore({ model: PasswordCredential, primaryKey: 'userName' }))
21
- .addStore(new InMemoryStore({ model: User, primaryKey: 'username' }))
22
- .addStore(new InMemoryStore({ model: PasswordResetToken, primaryKey: 'token' }))
23
- getRepository(i).createDataSet(PasswordCredential, 'userName')
24
- getRepository(i).createDataSet(PasswordResetToken, 'token')
19
+ /**
20
+ * Binds a fresh in-memory implementation of the two credential stores. The
21
+ * production default factories throw on purpose; tests opt in to memory
22
+ * persistence explicitly per injector.
23
+ */
24
+ const useInMemorySecurityStores = (i: Injector) => {
25
+ i.bind(PasswordCredentialStore, () => new InMemoryStore({ model: PasswordCredential, primaryKey: 'userName' }))
26
+ i.bind(PasswordResetTokenStore, () => new InMemoryStore({ model: PasswordResetToken, primaryKey: 'token' }))
25
27
  }
26
28
 
27
29
  describe('PasswordAuthenticator', () => {
28
- it('Should be instantiated', async () => {
29
- await usingAsync(new Injector(), async (i) => {
30
- setupSecurityStores(i)
30
+ it('resolves to a configured authenticator when stores and policy are set up', async () => {
31
+ await usingAsync(createInjector(), async (i) => {
32
+ useInMemorySecurityStores(i)
31
33
  usePasswordPolicy(i)
32
- const auth = i.getInstance(PasswordAuthenticator)
33
- expect(auth).toBeInstanceOf(PasswordAuthenticator)
34
+ const auth = i.get(PasswordAuthenticator)
35
+ expect(auth.policyManager).toBeDefined()
36
+ expect(auth.hasher).toBeDefined()
34
37
  })
35
38
  })
36
39
 
37
- it('Should inherit policy from the extension method', async () => {
38
- await usingAsync(new Injector(), async (i) => {
39
- setupSecurityStores(i)
40
- usePasswordPolicy(i, {
41
- passwordComplexityRules: [createMinLengthComplexityRule(3)],
42
- })
43
- const auth = i.getInstance(PasswordAuthenticator)
44
- expect(auth).toBeInstanceOf(PasswordAuthenticator)
40
+ it('picks up complexity rules supplied via usePasswordPolicy', async () => {
41
+ await usingAsync(createInjector(), async (i) => {
42
+ useInMemorySecurityStores(i)
43
+ usePasswordPolicy(i, { passwordComplexityRules: [createMinLengthComplexityRule(3)] })
44
+ const auth = i.get(PasswordAuthenticator)
45
45
  expect(auth.policyManager.policy.passwordComplexityRules[0].name).toBe('minLength')
46
46
  })
47
47
  })
48
48
 
49
49
  describe('checkPasswordForUser', () => {
50
- it('Should return a truthy value for valid username and password', async () => {
51
- await usingAsync(new Injector(), async (i) => {
50
+ it('returns isValid=true for a valid username and password combination', async () => {
51
+ await usingAsync(createInjector(), async (i) => {
52
52
  const userName = randomString()
53
53
  const password = randomString()
54
+ useInMemorySecurityStores(i)
55
+ usePasswordPolicy(i)
56
+ const authenticator = i.get(PasswordAuthenticator)
57
+ const credential = await authenticator.hasher.createCredential(userName, password)
58
+ await i.get(PasswordCredentialStore).add(credential)
54
59
 
55
- setupSecurityStores(i)
56
- usePasswordPolicy(i, {})
57
- const authenticator = i.getInstance(PasswordAuthenticator)
58
- const { hasher } = authenticator
59
- const passwordStore = i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName')
60
-
61
- const entry = await hasher.createCredential(userName, password)
62
- await passwordStore.add(entry)
63
-
64
- const successResult = await authenticator.checkPasswordForUser(userName, password)
65
- expect(successResult.isValid).toBeTruthy()
60
+ expect((await authenticator.checkPasswordForUser(userName, password)).isValid).toBe(true)
66
61
  })
67
62
  })
68
63
 
69
- it('Should return a falsy value for invalid username', async () => {
70
- await usingAsync(new Injector(), async (i) => {
71
- const userName = randomString()
72
- const password = randomString()
73
- setupSecurityStores(i)
64
+ it('returns badUsernameOrPassword for an unknown username', async () => {
65
+ await usingAsync(createInjector(), async (i) => {
66
+ useInMemorySecurityStores(i)
74
67
  usePasswordPolicy(i)
75
- const authenticator = i.getInstance(PasswordAuthenticator)
76
- const { hasher } = authenticator
77
- const passwordStore = i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName')
68
+ const authenticator = i.get(PasswordAuthenticator)
69
+ const credential = await authenticator.hasher.createCredential('someone', 'pw')
70
+ await i.get(PasswordCredentialStore).add(credential)
78
71
 
79
- const entry = await hasher.createCredential(userName, password)
80
- await passwordStore.add(entry)
81
-
82
- const failResult = await authenticator.checkPasswordForUser('invalidUsername', password)
83
- expect(failResult.isValid).toBeFalsy()
84
- expect((failResult as PasswordCheckFailedResult).reason).toBe('badUsernameOrPassword')
72
+ const failure = (await authenticator.checkPasswordForUser('nobody', 'pw')) as PasswordCheckFailedResult
73
+ expect(failure.isValid).toBe(false)
74
+ expect(failure.reason).toBe('badUsernameOrPassword')
85
75
  })
86
76
  })
87
77
 
88
- it('Should return a falsy value for invalid password', async () => {
89
- await usingAsync(new Injector(), async (i) => {
78
+ it('returns badUsernameOrPassword for the wrong password', async () => {
79
+ await usingAsync(createInjector(), async (i) => {
90
80
  const userName = randomString()
91
- const password = randomString()
92
- setupSecurityStores(i)
81
+ useInMemorySecurityStores(i)
93
82
  usePasswordPolicy(i)
94
- const authenticator = i.getInstance(PasswordAuthenticator)
95
- const { hasher } = authenticator
96
- const passwordStore = i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName')
83
+ const authenticator = i.get(PasswordAuthenticator)
84
+ const credential = await authenticator.hasher.createCredential(userName, 'right')
85
+ await i.get(PasswordCredentialStore).add(credential)
97
86
 
98
- const entry = await hasher.createCredential(userName, password)
99
- await passwordStore.add(entry)
100
-
101
- const failResult = await authenticator.checkPasswordForUser(userName, 'someInvalidPassword')
102
- expect(failResult.isValid).toBeFalsy()
103
- expect((failResult as PasswordCheckFailedResult).reason).toBe('badUsernameOrPassword')
87
+ const failure = (await authenticator.checkPasswordForUser(userName, 'wrong')) as PasswordCheckFailedResult
88
+ expect(failure.isValid).toBe(false)
89
+ expect(failure.reason).toBe('badUsernameOrPassword')
104
90
  })
105
91
  })
106
92
 
107
- it('Should return a falsy value for an expired password', async () => {
108
- await usingAsync(new Injector(), async (i) => {
93
+ it('returns passwordExpired for a credential older than the configured expiration window', async () => {
94
+ await usingAsync(createInjector(), async (i) => {
109
95
  const userName = randomString()
110
96
  const password = randomString()
111
- setupSecurityStores(i)
97
+ useInMemorySecurityStores(i)
112
98
  usePasswordPolicy(i, { passwordExpirationDays: 1 })
113
- const authenticator = i.getInstance(PasswordAuthenticator)
114
- const { hasher } = authenticator
115
- const passwordStore = i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName')
116
-
117
- const entry = await hasher.createCredential(userName, password)
118
-
119
- const creationDate = new Date()
120
- creationDate.setDate(creationDate.getDate() - 2)
121
-
122
- entry.creationDate = creationDate.toISOString()
123
-
124
- await passwordStore.add(entry)
125
-
126
- const failResult = await authenticator.checkPasswordForUser(userName, password)
127
- expect(failResult.isValid).toBeFalsy()
128
- expect((failResult as PasswordCheckFailedResult).reason).toBe('passwordExpired')
99
+ const authenticator = i.get(PasswordAuthenticator)
100
+ const credential = await authenticator.hasher.createCredential(userName, password)
101
+ const olderCreationDate = new Date()
102
+ olderCreationDate.setDate(olderCreationDate.getDate() - 2)
103
+ credential.creationDate = olderCreationDate.toISOString()
104
+ await i.get(PasswordCredentialStore).add(credential)
105
+
106
+ const failure = (await authenticator.checkPasswordForUser(userName, password)) as PasswordCheckFailedResult
107
+ expect(failure.isValid).toBe(false)
108
+ expect(failure.reason).toBe('passwordExpired')
129
109
  })
130
110
  })
131
111
  })
112
+
132
113
  describe('setPasswordForUser', () => {
133
- it('Should be able to set the password for the user using the last password', async () => {
134
- await usingAsync(new Injector(), async (i) => {
114
+ it('rotates the credential and keeps only the latest entry', async () => {
115
+ await usingAsync(createInjector(), async (i) => {
135
116
  const userName = randomString()
136
117
  const lastPassword = randomString()
137
118
  const password = randomString()
138
- setupSecurityStores(i)
119
+ useInMemorySecurityStores(i)
139
120
  usePasswordPolicy(i)
140
- const authenticator = i.getInstance(PasswordAuthenticator)
141
- const { hasher } = authenticator
142
- const passwordStore = i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName')
143
- const policyManager = i.getInstance(SecurityPolicyManager)
144
- const pmSpy = vi.spyOn(policyManager, 'matchPasswordComplexityRules')
121
+ const authenticator = i.get(PasswordAuthenticator)
122
+ const store = i.get(PasswordCredentialStore)
123
+ const complexitySpy = vi.spyOn(i.get(SecurityPolicyManager), 'matchPasswordComplexityRules')
124
+ const credential = await authenticator.hasher.createCredential(userName, lastPassword)
125
+ await store.add(credential)
145
126
 
146
- const entry = await hasher.createCredential(userName, lastPassword)
147
-
148
- await passwordStore.add(entry)
149
127
  await authenticator.setPasswordForUser(userName, lastPassword, password)
150
128
 
151
- expect(pmSpy).toBeCalled()
152
-
153
- const entryCount = await passwordStore.count({ userName: { $eq: userName } })
154
-
155
- expect(entryCount).toBe(1) // old entry should be removed
156
-
157
- const successResult = await authenticator.checkPasswordForUser(userName, password)
158
- expect(successResult.isValid).toBeTruthy()
159
-
160
- // Password updated, cannot use the last password
161
- const failResult = await authenticator.checkPasswordForUser(userName, lastPassword)
162
- expect(failResult.isValid).toBeFalsy()
163
- expect((failResult as PasswordCheckFailedResult).reason).toBe('badUsernameOrPassword')
129
+ expect(complexitySpy).toHaveBeenCalled()
130
+ expect(await store.count({ userName: { $eq: userName } })).toBe(1)
131
+ expect((await authenticator.checkPasswordForUser(userName, password)).isValid).toBe(true)
132
+ expect(
133
+ ((await authenticator.checkPasswordForUser(userName, lastPassword)) as PasswordCheckFailedResult).reason,
134
+ ).toBe('badUsernameOrPassword')
164
135
  })
165
136
  })
166
137
 
167
- it('Should throw PasswordComplexityError if the complexity rules are not respected', async () => {
168
- await usingAsync(new Injector(), async (i) => {
138
+ it('throws PasswordComplexityError when the new password fails a complexity rule', async () => {
139
+ await usingAsync(createInjector(), async (i) => {
169
140
  const userName = randomString()
170
141
  const lastPassword = randomString()
171
- const password = ''
172
- setupSecurityStores(i)
142
+ useInMemorySecurityStores(i)
173
143
  usePasswordPolicy(i, { passwordComplexityRules: [createMinLengthComplexityRule(3)] })
174
- const authenticator = i.getInstance(PasswordAuthenticator)
175
- const { hasher } = authenticator
176
- const passwordStore = i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName')
177
- const policyManager = i.getInstance(SecurityPolicyManager)
178
- const pmSpy = vi.spyOn(policyManager, 'matchPasswordComplexityRules')
179
-
180
- const entry = await hasher.createCredential(userName, lastPassword)
181
-
182
- await passwordStore.add(entry)
144
+ const authenticator = i.get(PasswordAuthenticator)
145
+ const credential = await authenticator.hasher.createCredential(userName, lastPassword)
146
+ await i.get(PasswordCredentialStore).add(credential)
183
147
 
184
- await expect(authenticator.setPasswordForUser(userName, lastPassword, password)).rejects.toThrowError(
148
+ await expect(authenticator.setPasswordForUser(userName, lastPassword, '')).rejects.toBeInstanceOf(
185
149
  PasswordComplexityError,
186
150
  )
187
-
188
- expect(pmSpy).toBeCalled()
189
-
190
- const entryCount = await passwordStore.count({ userName: { $eq: userName } })
191
-
192
- expect(entryCount).toBe(1) // old entry should be removed
193
-
194
- const failResult = await authenticator.checkPasswordForUser(userName, password)
195
- // Password NOT updated, can use the last password
196
- expect(failResult.isValid).toBeFalsy()
197
- expect((failResult as PasswordCheckFailedResult).reason).toBe('badUsernameOrPassword')
198
-
199
- // Password NOT updated, cannot use the new password
200
- const successResult = await authenticator.checkPasswordForUser(userName, lastPassword)
201
- expect(successResult.isValid).toBeTruthy()
151
+ expect((await authenticator.checkPasswordForUser(userName, lastPassword)).isValid).toBe(true)
202
152
  })
203
153
  })
204
154
 
205
- it('Should throw UnauthenticatedError if the last password is not valid', async () => {
206
- await usingAsync(new Injector(), async (i) => {
155
+ it('throws UnauthenticatedError when the previous password does not verify', async () => {
156
+ await usingAsync(createInjector(), async (i) => {
207
157
  const userName = randomString()
208
158
  const lastPassword = randomString()
209
- const password = randomString()
210
- setupSecurityStores(i)
159
+ useInMemorySecurityStores(i)
211
160
  usePasswordPolicy(i)
212
- const authenticator = i.getInstance(PasswordAuthenticator)
213
- const { hasher } = authenticator
214
- const passwordStore = i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName')
215
- const policyManager = i.getInstance(SecurityPolicyManager)
216
- const pmSpy = vi.spyOn(policyManager, 'matchPasswordComplexityRules')
217
-
218
- const entry = await hasher.createCredential(userName, lastPassword)
161
+ const authenticator = i.get(PasswordAuthenticator)
162
+ const credential = await authenticator.hasher.createCredential(userName, lastPassword)
163
+ await i.get(PasswordCredentialStore).add(credential)
219
164
 
220
- await passwordStore.add(entry)
221
-
222
- await expect(authenticator.setPasswordForUser(userName, 'someBadLastPassword', password)).rejects.toThrowError(
165
+ await expect(authenticator.setPasswordForUser(userName, 'wrong', randomString())).rejects.toBeInstanceOf(
223
166
  UnauthenticatedError,
224
167
  )
225
-
226
- expect(pmSpy).toBeCalled()
227
-
228
- const entryCount = await passwordStore.count({ userName: { $eq: userName } })
229
-
230
- expect(entryCount).toBe(1) // old entry should be removed
231
-
232
- const failResult = await authenticator.checkPasswordForUser(userName, password)
233
- // Password NOT updated, can use the last password
234
- expect(failResult.isValid).toBeFalsy()
235
- expect((failResult as PasswordCheckFailedResult).reason).toBe('badUsernameOrPassword')
236
-
237
- // Password NOT updated, cannot use the new password
238
- const successResult = await authenticator.checkPasswordForUser(userName, lastPassword)
239
- expect(successResult.isValid).toBeTruthy()
168
+ expect((await authenticator.checkPasswordForUser(userName, lastPassword)).isValid).toBe(true)
240
169
  })
241
170
  })
242
171
  })
243
172
 
244
173
  describe('resetPasswordForUser', () => {
245
- it('Should reset the password with the valid token', async () => {
246
- await usingAsync(new Injector(), async (i) => {
174
+ it('rotates the credential when the token is valid', async () => {
175
+ await usingAsync(createInjector(), async (i) => {
247
176
  const userName = randomString()
248
177
  const lastPassword = randomString()
249
178
  const password = randomString()
250
179
  const token = randomString()
251
- setupSecurityStores(i)
180
+ useInMemorySecurityStores(i)
252
181
  usePasswordPolicy(i)
253
- const authenticator = i.getInstance(PasswordAuthenticator)
254
- const { hasher } = authenticator
255
- const passwordStore = i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName')
256
- const resetTokenStore = i.getInstance(StoreManager).getStoreFor(PasswordResetToken, 'token')
257
-
258
- const entry = await hasher.createCredential(userName, lastPassword)
259
- await passwordStore.add(entry)
260
- await resetTokenStore.add({ userName, createdAt: new Date().toISOString(), token })
182
+ const authenticator = i.get(PasswordAuthenticator)
183
+ const credential = await authenticator.hasher.createCredential(userName, lastPassword)
184
+ await i.get(PasswordCredentialStore).add(credential)
185
+ await i.get(PasswordResetTokenStore).add({ userName, createdAt: new Date().toISOString(), token })
261
186
 
262
187
  await authenticator.resetPasswordForUser(token, password)
263
188
 
264
- const successResult = await authenticator.checkPasswordForUser(userName, password)
265
- expect(successResult.isValid).toBeTruthy()
266
-
267
- // Password updated, cannot use the last password
268
- const failResult = await authenticator.checkPasswordForUser(userName, lastPassword)
269
- expect(failResult.isValid).toBeFalsy()
270
- expect((failResult as PasswordCheckFailedResult).reason).toBe('badUsernameOrPassword')
189
+ expect((await authenticator.checkPasswordForUser(userName, password)).isValid).toBe(true)
190
+ expect(
191
+ ((await authenticator.checkPasswordForUser(userName, lastPassword)) as PasswordCheckFailedResult).reason,
192
+ ).toBe('badUsernameOrPassword')
271
193
  })
272
194
  })
273
- it('Should throw PasswordComplexityError if the complexity rules are not respected', async () => {
274
- await usingAsync(new Injector(), async (i) => {
195
+
196
+ it('throws PasswordComplexityError and preserves the stored credential on complexity failure', async () => {
197
+ await usingAsync(createInjector(), async (i) => {
275
198
  const userName = randomString()
276
199
  const lastPassword = randomString()
277
- const password = ''
278
200
  const token = randomString()
279
- setupSecurityStores(i)
201
+ useInMemorySecurityStores(i)
280
202
  usePasswordPolicy(i, { passwordComplexityRules: [createMinLengthComplexityRule(3)] })
281
- const authenticator = i.getInstance(PasswordAuthenticator)
282
- const { hasher } = authenticator
283
- const passwordStore = i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName')
284
- const resetTokenStore = i.getInstance(StoreManager).getStoreFor(PasswordResetToken, 'token')
285
-
286
- const entry = await hasher.createCredential(userName, lastPassword)
287
- await passwordStore.add(entry)
288
- await resetTokenStore.add({ userName, createdAt: new Date().toISOString(), token })
289
-
290
- await expect(authenticator.resetPasswordForUser(token, password)).rejects.toThrowError(PasswordComplexityError)
291
- const successResult = await authenticator.checkPasswordForUser(userName, lastPassword)
292
- expect(successResult.isValid).toBeTruthy()
203
+ const authenticator = i.get(PasswordAuthenticator)
204
+ const credential = await authenticator.hasher.createCredential(userName, lastPassword)
205
+ await i.get(PasswordCredentialStore).add(credential)
206
+ await i.get(PasswordResetTokenStore).add({ userName, createdAt: new Date().toISOString(), token })
293
207
 
294
- // Password updated, cannot use the last password
295
- const failResult = await authenticator.checkPasswordForUser(userName, password)
296
- expect(failResult.isValid).toBeFalsy()
297
- expect((failResult as PasswordCheckFailedResult).reason).toBe('badUsernameOrPassword')
208
+ await expect(authenticator.resetPasswordForUser(token, '')).rejects.toBeInstanceOf(PasswordComplexityError)
209
+ expect((await authenticator.checkPasswordForUser(userName, lastPassword)).isValid).toBe(true)
298
210
  })
299
211
  })
300
- it('Should throw UnauthenticatedError if the token is not valid', async () => {
301
- await usingAsync(new Injector(), async (i) => {
302
- const userName = randomString()
303
- const lastPassword = randomString()
304
- const password = ''
305
- const token = randomString()
306
- setupSecurityStores(i)
307
- usePasswordPolicy(i)
308
- const authenticator = i.getInstance(PasswordAuthenticator)
309
- const { hasher } = authenticator
310
- const passwordStore = i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName')
311
- const resetTokenStore = i.getInstance(StoreManager).getStoreFor(PasswordResetToken, 'token')
312
-
313
- const entry = await hasher.createCredential(userName, lastPassword)
314
- await passwordStore.add(entry)
315
- await resetTokenStore.add({ userName, createdAt: new Date().toISOString(), token })
316
212
 
317
- await expect(authenticator.resetPasswordForUser('someBadToken', password)).rejects.toThrowError(
318
- UnauthenticatedError,
319
- )
320
- const successResult = await authenticator.checkPasswordForUser(userName, lastPassword)
321
- expect(successResult.isValid).toBeTruthy()
213
+ it('throws UnauthenticatedError when the token does not exist', async () => {
214
+ await usingAsync(createInjector(), async (i) => {
215
+ useInMemorySecurityStores(i)
216
+ usePasswordPolicy(i)
217
+ const authenticator = i.get(PasswordAuthenticator)
322
218
 
323
- // Password updated, cannot use the last password
324
- const failResult = await authenticator.checkPasswordForUser(userName, password)
325
- expect(failResult.isValid).toBeFalsy()
326
- expect((failResult as PasswordCheckFailedResult).reason).toBe('badUsernameOrPassword')
219
+ await expect(authenticator.resetPasswordForUser('unknown', 'pw')).rejects.toBeInstanceOf(UnauthenticatedError)
327
220
  })
328
221
  })
329
- it('Should throw UnauthenticatedError and clean up token if the token has been expired', async () => {
330
- await usingAsync(new Injector(), async (i) => {
222
+
223
+ it('throws UnauthenticatedError and removes the expired token', async () => {
224
+ await usingAsync(createInjector(), async (i) => {
331
225
  const userName = randomString()
332
226
  const lastPassword = randomString()
333
- const password = ''
334
227
  const token = randomString()
335
- setupSecurityStores(i)
228
+ useInMemorySecurityStores(i)
336
229
  usePasswordPolicy(i, { resetTokenExpirationSeconds: 1 })
337
- const authenticator = i.getInstance(PasswordAuthenticator)
338
- const { hasher } = authenticator
339
- const passwordStore = i.getInstance(StoreManager).getStoreFor(PasswordCredential, 'userName')
340
- const resetTokenStore = i.getInstance(StoreManager).getStoreFor(PasswordResetToken, 'token')
341
-
342
- const entry = await hasher.createCredential(userName, lastPassword)
343
- await passwordStore.add(entry)
344
-
345
- const creationDate = new Date()
346
- creationDate.setDate(creationDate.getDate() - 2)
347
-
348
- await resetTokenStore.add({ userName, createdAt: creationDate.toISOString(), token })
349
-
350
- await expect(authenticator.resetPasswordForUser(token, password)).rejects.toThrowError(UnauthenticatedError)
351
- const successResult = await authenticator.checkPasswordForUser(userName, lastPassword)
352
- expect(successResult.isValid).toBeTruthy()
353
-
354
- // Password updated, cannot use the last password
355
- const failResult = await authenticator.checkPasswordForUser(userName, password)
356
- expect(failResult.isValid).toBeFalsy()
357
- expect((failResult as PasswordCheckFailedResult).reason).toBe('badUsernameOrPassword')
358
-
359
- const tokenCount = await resetTokenStore.count()
360
- expect(tokenCount).toBe(0)
230
+ const authenticator = i.get(PasswordAuthenticator)
231
+ const credential = await authenticator.hasher.createCredential(userName, lastPassword)
232
+ await i.get(PasswordCredentialStore).add(credential)
233
+ const pastCreatedAt = new Date()
234
+ pastCreatedAt.setDate(pastCreatedAt.getDate() - 2)
235
+ await i.get(PasswordResetTokenStore).add({ userName, createdAt: pastCreatedAt.toISOString(), token })
236
+
237
+ await expect(authenticator.resetPasswordForUser(token, randomString())).rejects.toBeInstanceOf(
238
+ UnauthenticatedError,
239
+ )
240
+ expect(await i.get(PasswordResetTokenStore).count()).toBe(0)
361
241
  })
362
242
  })
363
243
  })