@furystack/rest-service 11.0.7 → 12.1.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 +120 -0
- package/README.md +3 -2
- package/esm/actions/index.d.ts +1 -0
- package/esm/actions/index.d.ts.map +1 -1
- package/esm/actions/index.js +1 -0
- package/esm/actions/index.js.map +1 -1
- package/esm/actions/login.d.ts +7 -3
- package/esm/actions/login.d.ts.map +1 -1
- package/esm/actions/login.js +11 -5
- package/esm/actions/login.js.map +1 -1
- package/esm/actions/password-login-action.d.ts +24 -0
- package/esm/actions/password-login-action.d.ts.map +1 -0
- package/esm/actions/password-login-action.js +31 -0
- package/esm/actions/password-login-action.js.map +1 -0
- package/esm/actions/password-login-action.spec.d.ts +2 -0
- package/esm/actions/password-login-action.spec.d.ts.map +1 -0
- package/esm/actions/password-login-action.spec.js +105 -0
- package/esm/actions/password-login-action.spec.js.map +1 -0
- package/esm/authenticate.d.ts.map +1 -1
- package/esm/authenticate.js +4 -1
- package/esm/authenticate.js.map +1 -1
- package/esm/authenticate.spec.js +6 -4
- package/esm/authenticate.spec.js.map +1 -1
- package/esm/authentication-providers/authentication-provider.d.ts +25 -0
- package/esm/authentication-providers/authentication-provider.d.ts.map +1 -0
- package/esm/authentication-providers/authentication-provider.js +2 -0
- package/esm/authentication-providers/authentication-provider.js.map +1 -0
- package/esm/authentication-providers/basic-auth-provider.d.ts +8 -0
- package/esm/authentication-providers/basic-auth-provider.d.ts.map +1 -0
- package/esm/authentication-providers/basic-auth-provider.js +18 -0
- package/esm/authentication-providers/basic-auth-provider.js.map +1 -0
- package/esm/authentication-providers/cookie-auth-provider.d.ts +11 -0
- package/esm/authentication-providers/cookie-auth-provider.d.ts.map +1 -0
- package/esm/authentication-providers/cookie-auth-provider.js +21 -0
- package/esm/authentication-providers/cookie-auth-provider.js.map +1 -0
- package/esm/authentication-providers/helpers.d.ts +11 -0
- package/esm/authentication-providers/helpers.d.ts.map +1 -0
- package/esm/authentication-providers/helpers.js +47 -0
- package/esm/authentication-providers/helpers.js.map +1 -0
- package/esm/authentication-providers/index.d.ts +5 -0
- package/esm/authentication-providers/index.d.ts.map +1 -0
- package/esm/authentication-providers/index.js +5 -0
- package/esm/authentication-providers/index.js.map +1 -0
- package/esm/endpoint-generators/utils.d.ts.map +1 -1
- package/esm/endpoint-generators/utils.js +4 -1
- package/esm/endpoint-generators/utils.js.map +1 -1
- package/esm/helpers.d.ts +5 -2
- package/esm/helpers.d.ts.map +1 -1
- package/esm/helpers.js +27 -3
- package/esm/helpers.js.map +1 -1
- package/esm/helpers.spec.js +37 -0
- package/esm/helpers.spec.js.map +1 -1
- package/esm/http-authentication-settings.d.ts +11 -4
- package/esm/http-authentication-settings.d.ts.map +1 -1
- package/esm/http-authentication-settings.js +9 -2
- package/esm/http-authentication-settings.js.map +1 -1
- package/esm/http-user-context.d.ts +9 -4
- package/esm/http-user-context.d.ts.map +1 -1
- package/esm/http-user-context.js +28 -55
- package/esm/http-user-context.js.map +1 -1
- package/esm/http-user-context.spec.d.ts +3 -1
- package/esm/http-user-context.spec.d.ts.map +1 -1
- package/esm/http-user-context.spec.js +103 -45
- package/esm/http-user-context.spec.js.map +1 -1
- package/esm/index.d.ts +2 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +2 -0
- package/esm/index.js.map +1 -1
- package/esm/login-response-strategy.d.ts +28 -0
- package/esm/login-response-strategy.d.ts.map +1 -0
- package/esm/login-response-strategy.js +28 -0
- package/esm/login-response-strategy.js.map +1 -0
- package/esm/login-response-strategy.spec.d.ts +2 -0
- package/esm/login-response-strategy.spec.d.ts.map +1 -0
- package/esm/login-response-strategy.spec.js +78 -0
- package/esm/login-response-strategy.spec.js.map +1 -0
- package/esm/rest-service.integration.spec.d.ts.map +1 -1
- package/esm/rest-service.integration.spec.js +5 -4
- package/esm/rest-service.integration.spec.js.map +1 -1
- package/esm/validate.integration.spec.js +5 -0
- package/esm/validate.integration.spec.js.map +1 -1
- package/package.json +7 -7
- package/src/actions/index.ts +1 -0
- package/src/actions/login.ts +12 -6
- package/src/actions/password-login-action.spec.ts +122 -0
- package/src/actions/password-login-action.ts +35 -0
- package/src/authenticate.spec.ts +6 -4
- package/src/authenticate.ts +4 -1
- package/src/authentication-providers/authentication-provider.ts +25 -0
- package/src/authentication-providers/basic-auth-provider.ts +21 -0
- package/src/authentication-providers/cookie-auth-provider.ts +26 -0
- package/src/authentication-providers/helpers.ts +73 -0
- package/src/authentication-providers/index.ts +4 -0
- package/src/endpoint-generators/utils.ts +4 -1
- package/src/helpers.spec.ts +40 -0
- package/src/helpers.ts +48 -3
- package/src/http-authentication-settings.ts +30 -21
- package/src/http-user-context.spec.ts +462 -394
- package/src/http-user-context.ts +164 -194
- package/src/index.ts +2 -0
- package/src/login-response-strategy.spec.ts +90 -0
- package/src/login-response-strategy.ts +48 -0
- package/src/rest-service.integration.spec.ts +5 -4
- package/src/validate.integration.spec.ts +5 -0
|
@@ -1,394 +1,462 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
2
|
-
import { InMemoryStore, StoreManager, User, addStore } from '@furystack/core'
|
|
3
|
-
import { Injector } from '@furystack/inject'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
ctx
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
await
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
expect(
|
|
197
|
-
})
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
).
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
await
|
|
218
|
-
const ctx = i.getInstance(HttpUserContext)
|
|
219
|
-
await expect(
|
|
220
|
-
ctx.authenticateRequest({
|
|
221
|
-
headers: {
|
|
222
|
-
} as IncomingMessage),
|
|
223
|
-
).rejects.toThrowError(UnauthenticatedError)
|
|
224
|
-
})
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
it('Should fail with
|
|
228
|
-
await usingAsync(new Injector(), async (i) => {
|
|
229
|
-
await prepareInjector(i)
|
|
230
|
-
const ctx = i.getInstance(HttpUserContext)
|
|
231
|
-
await
|
|
232
|
-
.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
await
|
|
248
|
-
.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
await
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
it('Should
|
|
279
|
-
await usingAsync(new Injector(), async (i) => {
|
|
280
|
-
await prepareInjector(i)
|
|
281
|
-
const ctx = i.getInstance(HttpUserContext)
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
ctx.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
expect(
|
|
290
|
-
expect(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
ctx.
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
expect(
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
await
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
await
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
1
|
+
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
2
|
+
import { InMemoryStore, StoreManager, User, addStore, useSystemIdentityContext } from '@furystack/core'
|
|
3
|
+
import { Injector } from '@furystack/inject'
|
|
4
|
+
import { getDataSetFor, getRepository } from '@furystack/repository'
|
|
5
|
+
import {
|
|
6
|
+
PasswordAuthenticator,
|
|
7
|
+
PasswordCredential,
|
|
8
|
+
PasswordResetToken,
|
|
9
|
+
UnauthenticatedError,
|
|
10
|
+
usePasswordPolicy,
|
|
11
|
+
} from '@furystack/security'
|
|
12
|
+
import { usingAsync } from '@furystack/utils'
|
|
13
|
+
import type { IncomingMessage, ServerResponse } from 'http'
|
|
14
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
15
|
+
import { useHttpAuthentication } from './helpers.js'
|
|
16
|
+
import { HttpUserContext } from './http-user-context.js'
|
|
17
|
+
import { DefaultSession } from './models/default-session.js'
|
|
18
|
+
|
|
19
|
+
export const prepareInjector = async (i: Injector, options?: { enableBasicAuth?: boolean }) => {
|
|
20
|
+
addStore(i, new InMemoryStore({ model: User, primaryKey: 'username' }))
|
|
21
|
+
.addStore(new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }))
|
|
22
|
+
.addStore(new InMemoryStore({ model: PasswordCredential, primaryKey: 'userName' }))
|
|
23
|
+
.addStore(new InMemoryStore({ model: PasswordResetToken, primaryKey: 'token' }))
|
|
24
|
+
|
|
25
|
+
const repo = getRepository(i)
|
|
26
|
+
repo.createDataSet(User, 'username')
|
|
27
|
+
repo.createDataSet(DefaultSession, 'sessionId')
|
|
28
|
+
repo.createDataSet(PasswordCredential, 'userName')
|
|
29
|
+
repo.createDataSet(PasswordResetToken, 'token')
|
|
30
|
+
|
|
31
|
+
usePasswordPolicy(i)
|
|
32
|
+
useHttpAuthentication(i, { enableBasicAuth: options?.enableBasicAuth ?? true })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const setupUser = async (i: Injector, userName: string, password: string) => {
|
|
36
|
+
const sm = i.getInstance(StoreManager)
|
|
37
|
+
const pw = i.getInstance(PasswordAuthenticator)
|
|
38
|
+
const cred = await pw.hasher.createCredential(userName, password)
|
|
39
|
+
await sm.getStoreFor(PasswordCredential, 'userName').add(cred)
|
|
40
|
+
await sm.getStoreFor(User, 'username').add({ username: userName, roles: [] })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('HttpUserContext', () => {
|
|
44
|
+
const request = { headers: {} } as IncomingMessage
|
|
45
|
+
const response = {} as any as ServerResponse
|
|
46
|
+
|
|
47
|
+
const testUser: User = { username: 'testUser', roles: ['grantedRole1', 'grantedRole2'] }
|
|
48
|
+
|
|
49
|
+
it('Should be constructed with the extension method', async () => {
|
|
50
|
+
await usingAsync(new Injector(), async (i) => {
|
|
51
|
+
await prepareInjector(i)
|
|
52
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
53
|
+
expect(ctx).toBeInstanceOf(HttpUserContext)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
describe('isAuthenticated', () => {
|
|
58
|
+
it('Should return true for authenticated users', async () => {
|
|
59
|
+
await usingAsync(new Injector(), async (i) => {
|
|
60
|
+
await prepareInjector(i)
|
|
61
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
62
|
+
ctx.getCurrentUser = vi.fn(async () => testUser)
|
|
63
|
+
const value = await ctx.isAuthenticated(request)
|
|
64
|
+
expect(value).toBe(true)
|
|
65
|
+
expect(ctx.getCurrentUser).toBeCalled()
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('Should return false for unauthenticated users', async () => {
|
|
70
|
+
await usingAsync(new Injector(), async (i) => {
|
|
71
|
+
await prepareInjector(i)
|
|
72
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
73
|
+
ctx.getCurrentUser = vi.fn(async () => {
|
|
74
|
+
throw Error(':(')
|
|
75
|
+
})
|
|
76
|
+
await expect(ctx.isAuthenticated(request)).resolves.toEqual(false)
|
|
77
|
+
expect(ctx.getCurrentUser).toBeCalled()
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('isAuthorized', () => {
|
|
83
|
+
it('Should return true if all roles are authorized', async () => {
|
|
84
|
+
await usingAsync(new Injector(), async (i) => {
|
|
85
|
+
await prepareInjector(i)
|
|
86
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
87
|
+
ctx.getCurrentUser = vi.fn(async () => testUser)
|
|
88
|
+
const value = await ctx.isAuthorized(request, 'grantedRole1', 'grantedRole2')
|
|
89
|
+
expect(value).toBe(true)
|
|
90
|
+
expect(ctx.getCurrentUser).toBeCalled()
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('Should return false if not all roles are authorized', async () => {
|
|
95
|
+
await usingAsync(new Injector(), async (i) => {
|
|
96
|
+
await prepareInjector(i)
|
|
97
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
98
|
+
ctx.getCurrentUser = vi.fn(async () => testUser)
|
|
99
|
+
const value = await ctx.isAuthorized(request, 'grantedRole1', 'nonGrantedRole2')
|
|
100
|
+
expect(value).toBe(false)
|
|
101
|
+
expect(ctx.getCurrentUser).toBeCalled()
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('authenticateUser', () => {
|
|
107
|
+
it('Should fail when the store is empty', async () => {
|
|
108
|
+
await usingAsync(new Injector(), async (i) => {
|
|
109
|
+
await prepareInjector(i)
|
|
110
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
111
|
+
await expect(ctx.authenticateUser('user', 'password')).rejects.toThrowError(UnauthenticatedError)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('Should fail when the password not equals', async () => {
|
|
116
|
+
await usingAsync(new Injector(), async (i) => {
|
|
117
|
+
await prepareInjector(i)
|
|
118
|
+
await setupUser(i, 'user', 'pass123')
|
|
119
|
+
await expect(i.getInstance(HttpUserContext).authenticateUser('user', 'pass321')).rejects.toThrowError(
|
|
120
|
+
UnauthenticatedError,
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('Should fail when the username not equals', async () => {
|
|
126
|
+
await usingAsync(new Injector(), async (i) => {
|
|
127
|
+
await prepareInjector(i)
|
|
128
|
+
await setupUser(i, 'otherUser', 'pass123')
|
|
129
|
+
await expect(i.getInstance(HttpUserContext).authenticateUser('user', 'pass123')).rejects.toThrowError(
|
|
130
|
+
UnauthenticatedError,
|
|
131
|
+
)
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('Should fail when password not provided', async () => {
|
|
136
|
+
await usingAsync(new Injector(), async (i) => {
|
|
137
|
+
await prepareInjector(i)
|
|
138
|
+
await setupUser(i, 'user', 'pass123')
|
|
139
|
+
await expect(i.getInstance(HttpUserContext).authenticateUser('user', '')).rejects.toThrowError(
|
|
140
|
+
UnauthenticatedError,
|
|
141
|
+
)
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('Should fail when the user is not in the user store', async () => {
|
|
146
|
+
await usingAsync(new Injector(), async (i) => {
|
|
147
|
+
await prepareInjector(i)
|
|
148
|
+
await setupUser(i, 'user', 'pass123')
|
|
149
|
+
await i.getInstance(StoreManager).getStoreFor(User, 'username').remove('user')
|
|
150
|
+
await expect(i.getInstance(HttpUserContext).authenticateUser('user', 'pass123')).rejects.toThrowError(
|
|
151
|
+
UnauthenticatedError,
|
|
152
|
+
)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('Should return the user when the username and password matches', async () => {
|
|
157
|
+
await usingAsync(new Injector(), async (i) => {
|
|
158
|
+
await prepareInjector(i)
|
|
159
|
+
await setupUser(i, 'user', 'pass123')
|
|
160
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
161
|
+
const value = await ctx.authenticateUser('user', 'pass123')
|
|
162
|
+
expect(value).toEqual({ username: 'user', roles: [] })
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('getSessionIdFromRequest', () => {
|
|
168
|
+
it('Should return null if no headers present', async () => {
|
|
169
|
+
await usingAsync(new Injector(), async (i) => {
|
|
170
|
+
await prepareInjector(i)
|
|
171
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
172
|
+
const sid = ctx.getSessionIdFromRequest(request)
|
|
173
|
+
expect(sid).toBeNull()
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('Should return null if no session ID cookie present', async () => {
|
|
178
|
+
await usingAsync(new Injector(), async (i) => {
|
|
179
|
+
await prepareInjector(i)
|
|
180
|
+
const requestWithCookie = { ...request, cookie: 'a=2;b=3;c=4;' } as unknown as IncomingMessage
|
|
181
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
182
|
+
const sid = ctx.getSessionIdFromRequest(requestWithCookie)
|
|
183
|
+
expect(sid).toBeNull()
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
it('Should return the Session ID value if session ID cookie present', async () => {
|
|
187
|
+
await usingAsync(new Injector(), async (i) => {
|
|
188
|
+
await prepareInjector(i)
|
|
189
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
190
|
+
const requestWithAuthCookie = {
|
|
191
|
+
...request,
|
|
192
|
+
headers: { cookie: `a=2;b=3;${ctx.authentication.cookieName}=666;c=4;` },
|
|
193
|
+
} as unknown as IncomingMessage
|
|
194
|
+
|
|
195
|
+
const sid = ctx.getSessionIdFromRequest(requestWithAuthCookie)
|
|
196
|
+
expect(sid).toBe('666')
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
describe('authenticateRequest', () => {
|
|
202
|
+
it('Should authenticate with Basic Auth when enabled and valid credentials provided', async () => {
|
|
203
|
+
await usingAsync(new Injector(), async (i) => {
|
|
204
|
+
await prepareInjector(i)
|
|
205
|
+
await setupUser(i, 'testuser', 'password')
|
|
206
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
207
|
+
const result = await ctx.authenticateRequest({
|
|
208
|
+
headers: { authorization: `Basic dGVzdHVzZXI6cGFzc3dvcmQ=` },
|
|
209
|
+
} as IncomingMessage)
|
|
210
|
+
expect(result.username).toBe('testuser')
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it('Should NOT try to authenticate with Basic when disabled', async () => {
|
|
215
|
+
await usingAsync(new Injector(), async (i) => {
|
|
216
|
+
await prepareInjector(i, { enableBasicAuth: false })
|
|
217
|
+
await setupUser(i, 'testuser', 'password')
|
|
218
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
219
|
+
await expect(
|
|
220
|
+
ctx.authenticateRequest({
|
|
221
|
+
headers: { authorization: `Basic dGVzdHVzZXI6cGFzc3dvcmQ=` },
|
|
222
|
+
} as IncomingMessage),
|
|
223
|
+
).rejects.toThrowError(UnauthenticatedError)
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('Should fail with no session in the store', async () => {
|
|
228
|
+
await usingAsync(new Injector(), async (i) => {
|
|
229
|
+
await prepareInjector(i)
|
|
230
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
231
|
+
await expect(
|
|
232
|
+
ctx.authenticateRequest({
|
|
233
|
+
headers: { cookie: `${ctx.authentication.cookieName}=666;a=3` },
|
|
234
|
+
} as IncomingMessage),
|
|
235
|
+
).rejects.toThrowError(UnauthenticatedError)
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('Should fail with valid session Id but no user', async () => {
|
|
240
|
+
await usingAsync(new Injector(), async (i) => {
|
|
241
|
+
await prepareInjector(i)
|
|
242
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
243
|
+
await i.getInstance(StoreManager).getStoreFor(DefaultSession, 'sessionId').add({
|
|
244
|
+
sessionId: '666',
|
|
245
|
+
username: testUser.username,
|
|
246
|
+
})
|
|
247
|
+
await expect(
|
|
248
|
+
ctx.authenticateRequest({
|
|
249
|
+
headers: { cookie: `${ctx.authentication.cookieName}=666;a=3` },
|
|
250
|
+
} as IncomingMessage),
|
|
251
|
+
).rejects.toThrowError(UnauthenticatedError)
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('Should authenticate with cookie, if the session IDs matches', async () => {
|
|
256
|
+
await usingAsync(new Injector(), async (i) => {
|
|
257
|
+
await prepareInjector(i)
|
|
258
|
+
|
|
259
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
260
|
+
await i.getInstance(StoreManager).getStoreFor(DefaultSession, 'sessionId').add({
|
|
261
|
+
sessionId: '666',
|
|
262
|
+
username: testUser.username,
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
await i
|
|
266
|
+
.getInstance(StoreManager)
|
|
267
|
+
.getStoreFor(User, 'username')
|
|
268
|
+
.add({ ...testUser })
|
|
269
|
+
|
|
270
|
+
const result = await ctx.authenticateRequest({
|
|
271
|
+
headers: { cookie: `${ctx.authentication.cookieName}=666;a=3` },
|
|
272
|
+
} as IncomingMessage)
|
|
273
|
+
|
|
274
|
+
expect(result).toEqual(testUser)
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('Should iterate providers and return null results pass to next', async () => {
|
|
279
|
+
await usingAsync(new Injector(), async (i) => {
|
|
280
|
+
await prepareInjector(i)
|
|
281
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
282
|
+
const provider1 = vi.fn(async () => null)
|
|
283
|
+
const provider2 = vi.fn(async () => testUser)
|
|
284
|
+
ctx.authentication.authenticationProviders = [
|
|
285
|
+
{ name: 'test-1', authenticate: provider1 },
|
|
286
|
+
{ name: 'test-2', authenticate: provider2 },
|
|
287
|
+
]
|
|
288
|
+
const result = await ctx.authenticateRequest(request)
|
|
289
|
+
expect(provider1).toHaveBeenCalledOnce()
|
|
290
|
+
expect(provider2).toHaveBeenCalledOnce()
|
|
291
|
+
expect(result).toBe(testUser)
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('Should throw if provider throws (skipping remaining providers)', async () => {
|
|
296
|
+
await usingAsync(new Injector(), async (i) => {
|
|
297
|
+
await prepareInjector(i)
|
|
298
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
299
|
+
const provider1 = vi.fn(async () => {
|
|
300
|
+
throw new UnauthenticatedError()
|
|
301
|
+
})
|
|
302
|
+
const provider2 = vi.fn(async () => testUser)
|
|
303
|
+
ctx.authentication.authenticationProviders = [
|
|
304
|
+
{ name: 'test-1', authenticate: provider1 },
|
|
305
|
+
{ name: 'test-2', authenticate: provider2 },
|
|
306
|
+
]
|
|
307
|
+
await expect(ctx.authenticateRequest(request)).rejects.toThrowError(UnauthenticatedError)
|
|
308
|
+
expect(provider2).not.toHaveBeenCalled()
|
|
309
|
+
})
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('Should throw UnauthenticatedError if no provider returns a user', async () => {
|
|
313
|
+
await usingAsync(new Injector(), async (i) => {
|
|
314
|
+
await prepareInjector(i)
|
|
315
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
316
|
+
ctx.authentication.authenticationProviders = [{ name: 'test-1', authenticate: async () => null }]
|
|
317
|
+
await expect(ctx.authenticateRequest(request)).rejects.toThrowError(UnauthenticatedError)
|
|
318
|
+
})
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
describe('getCurrentUser', () => {
|
|
323
|
+
it('Should return the current user from authenticateRequest() once per request', async () => {
|
|
324
|
+
await usingAsync(new Injector(), async (i) => {
|
|
325
|
+
await prepareInjector(i)
|
|
326
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
327
|
+
ctx.authenticateRequest = vi.fn(async () => testUser)
|
|
328
|
+
const result = await ctx.getCurrentUser(request)
|
|
329
|
+
const result2 = await ctx.getCurrentUser(request)
|
|
330
|
+
expect(ctx.authenticateRequest).toBeCalledTimes(1)
|
|
331
|
+
expect(result).toBe(testUser)
|
|
332
|
+
expect(result2).toBe(testUser)
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
describe('cookieLogin', () => {
|
|
338
|
+
it('Should return the current user from authenticateRequest() once per request', async () => {
|
|
339
|
+
await usingAsync(new Injector(), async (i) => {
|
|
340
|
+
await prepareInjector(i)
|
|
341
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
342
|
+
const setHeader = vi.fn()
|
|
343
|
+
const addMock = vi.fn(async () => ({}))
|
|
344
|
+
// @ts-expect-error
|
|
345
|
+
ctx.getSessionDataSet = vi.fn(() => ({ add: addMock }))
|
|
346
|
+
const authResult = await ctx.cookieLogin(testUser, { setHeader })
|
|
347
|
+
expect(authResult).toBe(testUser)
|
|
348
|
+
expect(setHeader).toBeCalled()
|
|
349
|
+
expect(addMock).toBeCalled()
|
|
350
|
+
})
|
|
351
|
+
})
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
describe('cookieLogout', () => {
|
|
355
|
+
it('Should invalidate the current session id cookie', async () => {
|
|
356
|
+
await usingAsync(new Injector(), async (i) => {
|
|
357
|
+
await prepareInjector(i)
|
|
358
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
359
|
+
const setHeader = vi.fn()
|
|
360
|
+
const removeMock = vi.fn(async () => undefined)
|
|
361
|
+
const sessionDataSetMock = {
|
|
362
|
+
add: vi.fn(async () => ({})),
|
|
363
|
+
find: vi.fn(async () => [{ sessionId: 'example-session-id' }]),
|
|
364
|
+
remove: removeMock,
|
|
365
|
+
primaryKey: 'sessionId' as const,
|
|
366
|
+
}
|
|
367
|
+
// @ts-expect-error
|
|
368
|
+
ctx.getSessionDataSet = vi.fn(() => sessionDataSetMock)
|
|
369
|
+
ctx.authenticateRequest = vi.fn(async () => testUser)
|
|
370
|
+
ctx.getSessionIdFromRequest = () => 'example-session-id'
|
|
371
|
+
response.setHeader = vi.fn(() => response)
|
|
372
|
+
await ctx.cookieLogin(testUser, { setHeader })
|
|
373
|
+
await ctx.cookieLogout(request, response)
|
|
374
|
+
expect(response.setHeader).toBeCalledWith('Set-Cookie', 'fss=; Path=/; HttpOnly')
|
|
375
|
+
expect(removeMock).toBeCalled()
|
|
376
|
+
})
|
|
377
|
+
})
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
describe('Changes in the store during the context lifetime', () => {
|
|
381
|
+
it('Should update user roles', () => {
|
|
382
|
+
return usingAsync(new Injector(), async (i) => {
|
|
383
|
+
await prepareInjector(i)
|
|
384
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
385
|
+
const sm = i.getInstance(StoreManager)
|
|
386
|
+
await sm.getStoreFor(User, 'username').add(testUser)
|
|
387
|
+
|
|
388
|
+
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test')
|
|
389
|
+
await sm.getStoreFor(PasswordCredential, 'userName').add(pw)
|
|
390
|
+
|
|
391
|
+
await ctx.cookieLogin(testUser, { setHeader: vi.fn() })
|
|
392
|
+
|
|
393
|
+
const originalUser = await ctx.getCurrentUser(request)
|
|
394
|
+
expect(originalUser).toEqual(testUser)
|
|
395
|
+
|
|
396
|
+
const systemInjector = useSystemIdentityContext({ injector: i, username: 'test' })
|
|
397
|
+
const userDataSet = getDataSetFor(systemInjector, User, 'username')
|
|
398
|
+
const updatedUser = { ...testUser, roles: ['newFancyRole'] }
|
|
399
|
+
await userDataSet.update(systemInjector, testUser.username, updatedUser)
|
|
400
|
+
const updatedUserFromContext = await ctx.getCurrentUser(request)
|
|
401
|
+
expect(updatedUserFromContext.roles).toEqual(['newFancyRole'])
|
|
402
|
+
|
|
403
|
+
await userDataSet.update(systemInjector, testUser.username, { ...updatedUser, roles: [] })
|
|
404
|
+
const reloadedUserFromContext = await ctx.getCurrentUser(request)
|
|
405
|
+
expect(reloadedUserFromContext.roles).toEqual([])
|
|
406
|
+
})
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('Should remove current user when the user is removed from the store', () => {
|
|
410
|
+
return usingAsync(new Injector(), async (i) => {
|
|
411
|
+
await prepareInjector(i)
|
|
412
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
413
|
+
const sm = i.getInstance(StoreManager)
|
|
414
|
+
await sm.getStoreFor(User, 'username').add(testUser)
|
|
415
|
+
|
|
416
|
+
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test')
|
|
417
|
+
await sm.getStoreFor(PasswordCredential, 'userName').add(pw)
|
|
418
|
+
|
|
419
|
+
await ctx.cookieLogin(testUser, { setHeader: vi.fn() })
|
|
420
|
+
|
|
421
|
+
const originalUser = await ctx.getCurrentUser(request)
|
|
422
|
+
expect(originalUser).toEqual(testUser)
|
|
423
|
+
|
|
424
|
+
const systemInjector = useSystemIdentityContext({ injector: i, username: 'test' })
|
|
425
|
+
const userDataSet = getDataSetFor(systemInjector, User, 'username')
|
|
426
|
+
await userDataSet.remove(systemInjector, testUser.username)
|
|
427
|
+
|
|
428
|
+
await expect(() => ctx.getCurrentUser(request)).rejects.toThrowError(UnauthenticatedError)
|
|
429
|
+
})
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it('Should remove current user when the session is removed from the store', () => {
|
|
433
|
+
return usingAsync(new Injector(), async (i) => {
|
|
434
|
+
await prepareInjector(i)
|
|
435
|
+
const ctx = i.getInstance(HttpUserContext)
|
|
436
|
+
const sm = i.getInstance(StoreManager)
|
|
437
|
+
await sm.getStoreFor(User, 'username').add(testUser)
|
|
438
|
+
|
|
439
|
+
let sessionId = ''
|
|
440
|
+
|
|
441
|
+
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test')
|
|
442
|
+
await sm.getStoreFor(PasswordCredential, 'userName').add(pw)
|
|
443
|
+
|
|
444
|
+
await ctx.cookieLogin(testUser, {
|
|
445
|
+
setHeader: (_headerName, headerValue) => {
|
|
446
|
+
sessionId = headerValue.split('=')[1].split(';')[0]
|
|
447
|
+
return {} as ServerResponse
|
|
448
|
+
},
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
const originalUser = await ctx.getCurrentUser(request)
|
|
452
|
+
expect(originalUser).toEqual(testUser)
|
|
453
|
+
|
|
454
|
+
const systemInjector = useSystemIdentityContext({ injector: i, username: 'test' })
|
|
455
|
+
const sessionDataSet = getDataSetFor(systemInjector, DefaultSession, 'sessionId')
|
|
456
|
+
await sessionDataSet.remove(systemInjector, sessionId)
|
|
457
|
+
|
|
458
|
+
await expect(() => ctx.getCurrentUser(request)).rejects.toThrowError(UnauthenticatedError)
|
|
459
|
+
})
|
|
460
|
+
})
|
|
461
|
+
})
|
|
462
|
+
})
|