@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
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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('
|
|
29
|
-
await usingAsync(
|
|
30
|
-
|
|
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.
|
|
33
|
-
expect(auth).
|
|
34
|
+
const auth = i.get(PasswordAuthenticator)
|
|
35
|
+
expect(auth.policyManager).toBeDefined()
|
|
36
|
+
expect(auth.hasher).toBeDefined()
|
|
34
37
|
})
|
|
35
38
|
})
|
|
36
39
|
|
|
37
|
-
it('
|
|
38
|
-
await usingAsync(
|
|
39
|
-
|
|
40
|
-
usePasswordPolicy(i, {
|
|
41
|
-
|
|
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('
|
|
51
|
-
await usingAsync(
|
|
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
|
-
|
|
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('
|
|
70
|
-
await usingAsync(
|
|
71
|
-
|
|
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.
|
|
76
|
-
const
|
|
77
|
-
|
|
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
|
|
80
|
-
|
|
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('
|
|
89
|
-
await usingAsync(
|
|
78
|
+
it('returns badUsernameOrPassword for the wrong password', async () => {
|
|
79
|
+
await usingAsync(createInjector(), async (i) => {
|
|
90
80
|
const userName = randomString()
|
|
91
|
-
|
|
92
|
-
setupSecurityStores(i)
|
|
81
|
+
useInMemorySecurityStores(i)
|
|
93
82
|
usePasswordPolicy(i)
|
|
94
|
-
const authenticator = i.
|
|
95
|
-
const
|
|
96
|
-
|
|
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
|
|
99
|
-
|
|
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('
|
|
108
|
-
await usingAsync(
|
|
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
|
-
|
|
97
|
+
useInMemorySecurityStores(i)
|
|
112
98
|
usePasswordPolicy(i, { passwordExpirationDays: 1 })
|
|
113
|
-
const authenticator = i.
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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('
|
|
134
|
-
await usingAsync(
|
|
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
|
-
|
|
119
|
+
useInMemorySecurityStores(i)
|
|
139
120
|
usePasswordPolicy(i)
|
|
140
|
-
const authenticator = i.
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
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(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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('
|
|
168
|
-
await usingAsync(
|
|
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
|
-
|
|
172
|
-
setupSecurityStores(i)
|
|
142
|
+
useInMemorySecurityStores(i)
|
|
173
143
|
usePasswordPolicy(i, { passwordComplexityRules: [createMinLengthComplexityRule(3)] })
|
|
174
|
-
const authenticator = i.
|
|
175
|
-
const
|
|
176
|
-
|
|
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,
|
|
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('
|
|
206
|
-
await usingAsync(
|
|
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
|
-
|
|
210
|
-
setupSecurityStores(i)
|
|
159
|
+
useInMemorySecurityStores(i)
|
|
211
160
|
usePasswordPolicy(i)
|
|
212
|
-
const authenticator = i.
|
|
213
|
-
const
|
|
214
|
-
|
|
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
|
|
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('
|
|
246
|
-
await usingAsync(
|
|
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
|
-
|
|
180
|
+
useInMemorySecurityStores(i)
|
|
252
181
|
usePasswordPolicy(i)
|
|
253
|
-
const authenticator = i.
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
265
|
-
expect(
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
274
|
-
|
|
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
|
-
|
|
201
|
+
useInMemorySecurityStores(i)
|
|
280
202
|
usePasswordPolicy(i, { passwordComplexityRules: [createMinLengthComplexityRule(3)] })
|
|
281
|
-
const authenticator = i.
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
)
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
-
|
|
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
|
-
|
|
228
|
+
useInMemorySecurityStores(i)
|
|
336
229
|
usePasswordPolicy(i, { resetTokenExpirationSeconds: 1 })
|
|
337
|
-
const authenticator = i.
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
})
|