@furystack/rest-service 11.0.7 → 12.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 +68 -0
- package/README.md +3 -2
- 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 +1 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +1 -0
- package/esm/index.js.map +1 -1
- 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 +6 -6
- 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 +14 -5
- package/src/http-user-context.spec.ts +112 -44
- package/src/http-user-context.ts +27 -57
- package/src/index.ts +1 -0
- package/src/rest-service.integration.spec.ts +5 -4
- package/src/validate.integration.spec.ts +5 -0
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
2
|
-
import { InMemoryStore, StoreManager, User, addStore } from '@furystack/core'
|
|
2
|
+
import { InMemoryStore, StoreManager, User, addStore, useSystemIdentityContext } from '@furystack/core'
|
|
3
3
|
import { Injector } from '@furystack/inject'
|
|
4
|
-
import {
|
|
4
|
+
import { getDataSetFor, getRepository } from '@furystack/repository'
|
|
5
|
+
import {
|
|
6
|
+
PasswordAuthenticator,
|
|
7
|
+
PasswordCredential,
|
|
8
|
+
PasswordResetToken,
|
|
9
|
+
UnauthenticatedError,
|
|
10
|
+
usePasswordPolicy,
|
|
11
|
+
} from '@furystack/security'
|
|
5
12
|
import { usingAsync } from '@furystack/utils'
|
|
6
13
|
import type { IncomingMessage, ServerResponse } from 'http'
|
|
7
14
|
import { describe, expect, it, vi } from 'vitest'
|
|
@@ -9,12 +16,20 @@ import { useHttpAuthentication } from './helpers.js'
|
|
|
9
16
|
import { HttpUserContext } from './http-user-context.js'
|
|
10
17
|
import { DefaultSession } from './models/default-session.js'
|
|
11
18
|
|
|
12
|
-
export const prepareInjector = async (i: Injector) => {
|
|
19
|
+
export const prepareInjector = async (i: Injector, options?: { enableBasicAuth?: boolean }) => {
|
|
13
20
|
addStore(i, new InMemoryStore({ model: User, primaryKey: 'username' }))
|
|
14
21
|
.addStore(new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }))
|
|
15
22
|
.addStore(new InMemoryStore({ model: PasswordCredential, primaryKey: 'userName' }))
|
|
23
|
+
.addStore(new InMemoryStore({ model: PasswordResetToken, primaryKey: 'token' }))
|
|
16
24
|
|
|
17
|
-
|
|
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 })
|
|
18
33
|
}
|
|
19
34
|
|
|
20
35
|
const setupUser = async (i: Injector, userName: string, password: string) => {
|
|
@@ -184,31 +199,28 @@ describe('HttpUserContext', () => {
|
|
|
184
199
|
})
|
|
185
200
|
|
|
186
201
|
describe('authenticateRequest', () => {
|
|
187
|
-
it('Should
|
|
202
|
+
it('Should authenticate with Basic Auth when enabled and valid credentials provided', async () => {
|
|
188
203
|
await usingAsync(new Injector(), async (i) => {
|
|
189
204
|
await prepareInjector(i)
|
|
205
|
+
await setupUser(i, 'testuser', 'password')
|
|
190
206
|
const ctx = i.getInstance(HttpUserContext)
|
|
191
|
-
ctx.authenticateUser = vi.fn(async () => testUser)
|
|
192
207
|
const result = await ctx.authenticateRequest({
|
|
193
208
|
headers: { authorization: `Basic dGVzdHVzZXI6cGFzc3dvcmQ=` },
|
|
194
209
|
} as IncomingMessage)
|
|
195
|
-
expect(
|
|
196
|
-
expect(result).toBe(testUser)
|
|
210
|
+
expect(result.username).toBe('testuser')
|
|
197
211
|
})
|
|
198
212
|
})
|
|
199
213
|
|
|
200
|
-
it('Should NOT try to authenticate with Basic
|
|
214
|
+
it('Should NOT try to authenticate with Basic when disabled', async () => {
|
|
201
215
|
await usingAsync(new Injector(), async (i) => {
|
|
202
|
-
await prepareInjector(i)
|
|
216
|
+
await prepareInjector(i, { enableBasicAuth: false })
|
|
217
|
+
await setupUser(i, 'testuser', 'password')
|
|
203
218
|
const ctx = i.getInstance(HttpUserContext)
|
|
204
|
-
ctx.authentication.enableBasicAuth = false
|
|
205
|
-
ctx.authenticateUser = vi.fn(async () => testUser)
|
|
206
219
|
await expect(
|
|
207
220
|
ctx.authenticateRequest({
|
|
208
221
|
headers: { authorization: `Basic dGVzdHVzZXI6cGFzc3dvcmQ=` },
|
|
209
222
|
} as IncomingMessage),
|
|
210
223
|
).rejects.toThrowError(UnauthenticatedError)
|
|
211
|
-
expect(ctx.authenticateUser).not.toBeCalled()
|
|
212
224
|
})
|
|
213
225
|
})
|
|
214
226
|
|
|
@@ -228,9 +240,10 @@ describe('HttpUserContext', () => {
|
|
|
228
240
|
await usingAsync(new Injector(), async (i) => {
|
|
229
241
|
await prepareInjector(i)
|
|
230
242
|
const ctx = i.getInstance(HttpUserContext)
|
|
231
|
-
await
|
|
232
|
-
|
|
233
|
-
|
|
243
|
+
await i.getInstance(StoreManager).getStoreFor(DefaultSession, 'sessionId').add({
|
|
244
|
+
sessionId: '666',
|
|
245
|
+
username: testUser.username,
|
|
246
|
+
})
|
|
234
247
|
await expect(
|
|
235
248
|
ctx.authenticateRequest({
|
|
236
249
|
headers: { cookie: `${ctx.authentication.cookieName}=666;a=3` },
|
|
@@ -244,11 +257,15 @@ describe('HttpUserContext', () => {
|
|
|
244
257
|
await prepareInjector(i)
|
|
245
258
|
|
|
246
259
|
const ctx = i.getInstance(HttpUserContext)
|
|
247
|
-
await
|
|
248
|
-
|
|
249
|
-
|
|
260
|
+
await i.getInstance(StoreManager).getStoreFor(DefaultSession, 'sessionId').add({
|
|
261
|
+
sessionId: '666',
|
|
262
|
+
username: testUser.username,
|
|
263
|
+
})
|
|
250
264
|
|
|
251
|
-
await
|
|
265
|
+
await i
|
|
266
|
+
.getInstance(StoreManager)
|
|
267
|
+
.getStoreFor(User, 'username')
|
|
268
|
+
.add({ ...testUser })
|
|
252
269
|
|
|
253
270
|
const result = await ctx.authenticateRequest({
|
|
254
271
|
headers: { cookie: `${ctx.authentication.cookieName}=666;a=3` },
|
|
@@ -257,6 +274,49 @@ describe('HttpUserContext', () => {
|
|
|
257
274
|
expect(result).toEqual(testUser)
|
|
258
275
|
})
|
|
259
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
|
+
})
|
|
260
320
|
})
|
|
261
321
|
|
|
262
322
|
describe('getCurrentUser', () => {
|
|
@@ -280,14 +340,13 @@ describe('HttpUserContext', () => {
|
|
|
280
340
|
await prepareInjector(i)
|
|
281
341
|
const ctx = i.getInstance(HttpUserContext)
|
|
282
342
|
const setHeader = vi.fn()
|
|
343
|
+
const addMock = vi.fn(async () => ({}))
|
|
283
344
|
// @ts-expect-error
|
|
284
|
-
ctx.
|
|
285
|
-
return {}
|
|
286
|
-
})
|
|
345
|
+
ctx.getSessionDataSet = vi.fn(() => ({ add: addMock }))
|
|
287
346
|
const authResult = await ctx.cookieLogin(testUser, { setHeader })
|
|
288
347
|
expect(authResult).toBe(testUser)
|
|
289
348
|
expect(setHeader).toBeCalled()
|
|
290
|
-
expect(
|
|
349
|
+
expect(addMock).toBeCalled()
|
|
291
350
|
})
|
|
292
351
|
})
|
|
293
352
|
})
|
|
@@ -298,18 +357,22 @@ describe('HttpUserContext', () => {
|
|
|
298
357
|
await prepareInjector(i)
|
|
299
358
|
const ctx = i.getInstance(HttpUserContext)
|
|
300
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
|
+
}
|
|
301
367
|
// @ts-expect-error
|
|
302
|
-
ctx.
|
|
303
|
-
return {}
|
|
304
|
-
})
|
|
368
|
+
ctx.getSessionDataSet = vi.fn(() => sessionDataSetMock)
|
|
305
369
|
ctx.authenticateRequest = vi.fn(async () => testUser)
|
|
306
|
-
ctx.getSessionStore().remove = vi.fn(async () => undefined)
|
|
307
370
|
ctx.getSessionIdFromRequest = () => 'example-session-id'
|
|
308
371
|
response.setHeader = vi.fn(() => response)
|
|
309
372
|
await ctx.cookieLogin(testUser, { setHeader })
|
|
310
373
|
await ctx.cookieLogout(request, response)
|
|
311
374
|
expect(response.setHeader).toBeCalledWith('Set-Cookie', 'fss=; Path=/; HttpOnly')
|
|
312
|
-
expect(
|
|
375
|
+
expect(removeMock).toBeCalled()
|
|
313
376
|
})
|
|
314
377
|
})
|
|
315
378
|
})
|
|
@@ -319,23 +382,25 @@ describe('HttpUserContext', () => {
|
|
|
319
382
|
return usingAsync(new Injector(), async (i) => {
|
|
320
383
|
await prepareInjector(i)
|
|
321
384
|
const ctx = i.getInstance(HttpUserContext)
|
|
322
|
-
const
|
|
323
|
-
await
|
|
385
|
+
const sm = i.getInstance(StoreManager)
|
|
386
|
+
await sm.getStoreFor(User, 'username').add(testUser)
|
|
324
387
|
|
|
325
388
|
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test')
|
|
326
|
-
await
|
|
389
|
+
await sm.getStoreFor(PasswordCredential, 'userName').add(pw)
|
|
327
390
|
|
|
328
391
|
await ctx.cookieLogin(testUser, { setHeader: vi.fn() })
|
|
329
392
|
|
|
330
393
|
const originalUser = await ctx.getCurrentUser(request)
|
|
331
394
|
expect(originalUser).toEqual(testUser)
|
|
332
395
|
|
|
396
|
+
const systemInjector = useSystemIdentityContext({ injector: i, username: 'test' })
|
|
397
|
+
const userDataSet = getDataSetFor(systemInjector, User, 'username')
|
|
333
398
|
const updatedUser = { ...testUser, roles: ['newFancyRole'] }
|
|
334
|
-
await
|
|
399
|
+
await userDataSet.update(systemInjector, testUser.username, updatedUser)
|
|
335
400
|
const updatedUserFromContext = await ctx.getCurrentUser(request)
|
|
336
401
|
expect(updatedUserFromContext.roles).toEqual(['newFancyRole'])
|
|
337
402
|
|
|
338
|
-
await
|
|
403
|
+
await userDataSet.update(systemInjector, testUser.username, { ...updatedUser, roles: [] })
|
|
339
404
|
const reloadedUserFromContext = await ctx.getCurrentUser(request)
|
|
340
405
|
expect(reloadedUserFromContext.roles).toEqual([])
|
|
341
406
|
})
|
|
@@ -345,18 +410,20 @@ describe('HttpUserContext', () => {
|
|
|
345
410
|
return usingAsync(new Injector(), async (i) => {
|
|
346
411
|
await prepareInjector(i)
|
|
347
412
|
const ctx = i.getInstance(HttpUserContext)
|
|
348
|
-
const
|
|
349
|
-
await
|
|
413
|
+
const sm = i.getInstance(StoreManager)
|
|
414
|
+
await sm.getStoreFor(User, 'username').add(testUser)
|
|
350
415
|
|
|
351
416
|
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test')
|
|
352
|
-
await
|
|
417
|
+
await sm.getStoreFor(PasswordCredential, 'userName').add(pw)
|
|
353
418
|
|
|
354
419
|
await ctx.cookieLogin(testUser, { setHeader: vi.fn() })
|
|
355
420
|
|
|
356
421
|
const originalUser = await ctx.getCurrentUser(request)
|
|
357
422
|
expect(originalUser).toEqual(testUser)
|
|
358
423
|
|
|
359
|
-
|
|
424
|
+
const systemInjector = useSystemIdentityContext({ injector: i, username: 'test' })
|
|
425
|
+
const userDataSet = getDataSetFor(systemInjector, User, 'username')
|
|
426
|
+
await userDataSet.remove(systemInjector, testUser.username)
|
|
360
427
|
|
|
361
428
|
await expect(() => ctx.getCurrentUser(request)).rejects.toThrowError(UnauthenticatedError)
|
|
362
429
|
})
|
|
@@ -366,17 +433,17 @@ describe('HttpUserContext', () => {
|
|
|
366
433
|
return usingAsync(new Injector(), async (i) => {
|
|
367
434
|
await prepareInjector(i)
|
|
368
435
|
const ctx = i.getInstance(HttpUserContext)
|
|
369
|
-
const
|
|
370
|
-
await
|
|
436
|
+
const sm = i.getInstance(StoreManager)
|
|
437
|
+
await sm.getStoreFor(User, 'username').add(testUser)
|
|
371
438
|
|
|
372
439
|
let sessionId = ''
|
|
373
440
|
|
|
374
441
|
const pw = await i.getInstance(PasswordAuthenticator).hasher.createCredential(testUser.username, 'test')
|
|
375
|
-
await
|
|
442
|
+
await sm.getStoreFor(PasswordCredential, 'userName').add(pw)
|
|
376
443
|
|
|
377
444
|
await ctx.cookieLogin(testUser, {
|
|
378
445
|
setHeader: (_headerName, headerValue) => {
|
|
379
|
-
sessionId = headerValue
|
|
446
|
+
sessionId = headerValue.split('=')[1].split(';')[0]
|
|
380
447
|
return {} as ServerResponse
|
|
381
448
|
},
|
|
382
449
|
})
|
|
@@ -384,8 +451,9 @@ describe('HttpUserContext', () => {
|
|
|
384
451
|
const originalUser = await ctx.getCurrentUser(request)
|
|
385
452
|
expect(originalUser).toEqual(testUser)
|
|
386
453
|
|
|
387
|
-
const
|
|
388
|
-
|
|
454
|
+
const systemInjector = useSystemIdentityContext({ injector: i, username: 'test' })
|
|
455
|
+
const sessionDataSet = getDataSetFor(systemInjector, DefaultSession, 'sessionId')
|
|
456
|
+
await sessionDataSet.remove(systemInjector, sessionId)
|
|
389
457
|
|
|
390
458
|
await expect(() => ctx.getCurrentUser(request)).rejects.toThrowError(UnauthenticatedError)
|
|
391
459
|
})
|
package/src/http-user-context.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { User } from '@furystack/core'
|
|
2
|
-
import {
|
|
2
|
+
import { useSystemIdentityContext } from '@furystack/core'
|
|
3
|
+
import type { Injector } from '@furystack/inject'
|
|
3
4
|
import { Injectable, Injected } from '@furystack/inject'
|
|
4
5
|
import { PasswordAuthenticator, UnauthenticatedError } from '@furystack/security'
|
|
5
6
|
import { randomBytes } from 'crypto'
|
|
6
7
|
import type { IncomingMessage } from 'http'
|
|
8
|
+
import { extractSessionIdFromCookies } from './authentication-providers/helpers.js'
|
|
7
9
|
import { HttpAuthenticationSettings } from './http-authentication-settings.js'
|
|
8
10
|
import type { DefaultSession } from './models/default-session.js'
|
|
9
11
|
|
|
@@ -12,28 +14,19 @@ import type { DefaultSession } from './models/default-session.js'
|
|
|
12
14
|
*/
|
|
13
15
|
@Injectable({ lifetime: 'scoped' })
|
|
14
16
|
export class HttpUserContext {
|
|
15
|
-
public
|
|
17
|
+
public getUserDataSet = () => this.authentication.getUserDataSet(this.systemInjector)
|
|
16
18
|
|
|
17
|
-
public
|
|
19
|
+
public getSessionDataSet = () => this.authentication.getSessionDataSet(this.systemInjector)
|
|
18
20
|
|
|
19
21
|
private getUserByName = async (userName: string) => {
|
|
20
|
-
const
|
|
21
|
-
const users = await
|
|
22
|
+
const userDataSet = this.getUserDataSet()
|
|
23
|
+
const users = await userDataSet.find(this.systemInjector, { filter: { username: { $eq: userName } }, top: 2 })
|
|
22
24
|
if (users.length !== 1) {
|
|
23
25
|
throw new UnauthenticatedError()
|
|
24
26
|
}
|
|
25
27
|
return users[0]
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
private getSessionById = async (sessionId: string) => {
|
|
29
|
-
const sessionStore = this.getSessionStore()
|
|
30
|
-
const sessions = await sessionStore.find({ filter: { sessionId: { $eq: sessionId } }, top: 2 })
|
|
31
|
-
if (sessions.length !== 1) {
|
|
32
|
-
throw new UnauthenticatedError()
|
|
33
|
-
}
|
|
34
|
-
return sessions[0]
|
|
35
|
-
}
|
|
36
|
-
|
|
37
30
|
private user?: User
|
|
38
31
|
|
|
39
32
|
/**
|
|
@@ -93,43 +86,20 @@ export class HttpUserContext {
|
|
|
93
86
|
}
|
|
94
87
|
|
|
95
88
|
public getSessionIdFromRequest(request: Pick<IncomingMessage, 'headers'>): string | null {
|
|
96
|
-
|
|
97
|
-
const cookies = request.headers.cookie
|
|
98
|
-
.toString()
|
|
99
|
-
.split(';')
|
|
100
|
-
.filter((val) => val.length > 0)
|
|
101
|
-
.map((val) => {
|
|
102
|
-
const [name, value] = val.split('=')
|
|
103
|
-
return { name: name?.trim(), value: value?.trim() }
|
|
104
|
-
})
|
|
105
|
-
const sessionCookie = cookies.find((c) => c.name === this.authentication.cookieName)
|
|
106
|
-
if (sessionCookie) {
|
|
107
|
-
return sessionCookie.value
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return null
|
|
89
|
+
return extractSessionIdFromCookies(request, this.authentication.cookieName)
|
|
111
90
|
}
|
|
112
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Iterates registered authentication providers in order.
|
|
94
|
+
* - A provider returning `User` means authentication succeeded.
|
|
95
|
+
* - A provider returning `null` means it does not apply; try the next one.
|
|
96
|
+
* - A provider throwing means it owns the request but auth failed; propagate the error.
|
|
97
|
+
*/
|
|
113
98
|
public async authenticateRequest(request: Pick<IncomingMessage, 'headers'>): Promise<User> {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const [userName, password] = authData.toString().split(':')
|
|
118
|
-
return await this.authenticateUser(userName, password)
|
|
99
|
+
for (const provider of this.authentication.authenticationProviders) {
|
|
100
|
+
const user = await provider.authenticate(request)
|
|
101
|
+
if (user) return user
|
|
119
102
|
}
|
|
120
|
-
|
|
121
|
-
// Cookie auth
|
|
122
|
-
const sessionId = this.getSessionIdFromRequest(request)
|
|
123
|
-
if (sessionId) {
|
|
124
|
-
const session = await this.getSessionById(sessionId)
|
|
125
|
-
if (session) {
|
|
126
|
-
const user = await this.getUserByName(session.username)
|
|
127
|
-
if (user) {
|
|
128
|
-
return user
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
103
|
throw new UnauthenticatedError()
|
|
134
104
|
}
|
|
135
105
|
|
|
@@ -144,7 +114,7 @@ export class HttpUserContext {
|
|
|
144
114
|
serverResponse: { setHeader: (header: string, value: string) => void },
|
|
145
115
|
): Promise<User> {
|
|
146
116
|
const sessionId = randomBytes(32).toString('hex')
|
|
147
|
-
await this.
|
|
117
|
+
await this.getSessionDataSet().add(this.systemInjector, { sessionId, username: user.username })
|
|
148
118
|
serverResponse.setHeader('Set-Cookie', `${this.authentication.cookieName}=${sessionId}; Path=/; HttpOnly`)
|
|
149
119
|
this.user = user
|
|
150
120
|
return user
|
|
@@ -159,36 +129,36 @@ export class HttpUserContext {
|
|
|
159
129
|
response.setHeader('Set-Cookie', `${this.authentication.cookieName}=; Path=/; HttpOnly`)
|
|
160
130
|
|
|
161
131
|
if (sessionId) {
|
|
162
|
-
const
|
|
163
|
-
const sessions = await
|
|
164
|
-
await
|
|
132
|
+
const sessionDataSet = this.getSessionDataSet()
|
|
133
|
+
const sessions = await sessionDataSet.find(this.systemInjector, { filter: { sessionId: { $eq: sessionId } } })
|
|
134
|
+
await sessionDataSet.remove(this.systemInjector, ...sessions.map((s) => s[sessionDataSet.primaryKey]))
|
|
165
135
|
}
|
|
166
136
|
}
|
|
167
137
|
|
|
168
138
|
@Injected(HttpAuthenticationSettings)
|
|
169
139
|
declare public readonly authentication: HttpAuthenticationSettings<User, DefaultSession>
|
|
170
140
|
|
|
171
|
-
@Injected(
|
|
172
|
-
declare private readonly
|
|
141
|
+
@Injected((injector: Injector) => useSystemIdentityContext({ injector, username: 'HttpUserContext' }))
|
|
142
|
+
declare private readonly systemInjector: Injector
|
|
173
143
|
|
|
174
144
|
@Injected(PasswordAuthenticator)
|
|
175
145
|
declare private readonly authenticator: PasswordAuthenticator
|
|
176
146
|
|
|
177
147
|
public init() {
|
|
178
|
-
this.
|
|
148
|
+
this.getUserDataSet().addListener('onEntityUpdated', ({ id, change }) => {
|
|
179
149
|
if (this.user?.username === id) {
|
|
180
150
|
this.user = { ...this.user, ...change }
|
|
181
151
|
}
|
|
182
152
|
})
|
|
183
153
|
|
|
184
|
-
this.
|
|
154
|
+
this.getUserDataSet().addListener('onEntityRemoved', ({ key }) => {
|
|
185
155
|
if (this.user?.username === key) {
|
|
186
156
|
this.user = undefined
|
|
187
157
|
}
|
|
188
158
|
})
|
|
189
159
|
|
|
190
|
-
this.
|
|
191
|
-
this.user = undefined
|
|
160
|
+
this.getSessionDataSet().addListener('onEntityRemoved', () => {
|
|
161
|
+
this.user = undefined
|
|
192
162
|
})
|
|
193
163
|
}
|
|
194
164
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from './actions/index.js'
|
|
|
2
2
|
export * from './add-cors-header.js'
|
|
3
3
|
export * from './api-manager.js'
|
|
4
4
|
export * from './authenticate.js'
|
|
5
|
+
export * from './authentication-providers/index.js'
|
|
5
6
|
export * from './authorize.js'
|
|
6
7
|
export * from './endpoint-generators/index.js'
|
|
7
8
|
export * from './get-schema-from-api.js'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { InMemoryStore, User, addStore } from '@furystack/core'
|
|
2
2
|
import { getPort } from '@furystack/core/port-generator'
|
|
3
3
|
import { Injector } from '@furystack/inject'
|
|
4
|
+
import { getRepository } from '@furystack/repository'
|
|
4
5
|
import type { RestApi } from '@furystack/rest'
|
|
5
6
|
import { serializeValue } from '@furystack/rest'
|
|
6
7
|
import { PathHelper, usingAsync } from '@furystack/utils'
|
|
@@ -33,10 +34,10 @@ const createIntegrationApi = async () => {
|
|
|
33
34
|
addStore(i, new InMemoryStore({ model: User, primaryKey: 'username' })).addStore(
|
|
34
35
|
new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }),
|
|
35
36
|
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const repo = getRepository(i)
|
|
38
|
+
repo.createDataSet(User, 'username')
|
|
39
|
+
repo.createDataSet(DefaultSession, 'sessionId')
|
|
40
|
+
useHttpAuthentication(i)
|
|
40
41
|
await useRestService<IntegrationTestApi>({
|
|
41
42
|
injector: i,
|
|
42
43
|
root,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { getStoreManager, InMemoryStore, User } from '@furystack/core'
|
|
3
3
|
import { getPort } from '@furystack/core/port-generator'
|
|
4
4
|
import { Injector } from '@furystack/inject'
|
|
5
|
+
import { getRepository } from '@furystack/repository'
|
|
5
6
|
import type { SwaggerDocument, WithSchemaAction } from '@furystack/rest'
|
|
6
7
|
import { createClient, ResponseError } from '@furystack/rest-client-fetch'
|
|
7
8
|
import { usingAsync } from '@furystack/utils'
|
|
@@ -32,6 +33,10 @@ const createValidateApi = async (options = { enableGetSchema: false }) => {
|
|
|
32
33
|
|
|
33
34
|
getStoreManager(injector).addStore(new InMemoryStore({ model: User, primaryKey: 'username' }))
|
|
34
35
|
getStoreManager(injector).addStore(new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }))
|
|
36
|
+
getStoreManager(injector).addStore(new InMemoryStore({ model: MockClass, primaryKey: 'id' }))
|
|
37
|
+
getRepository(injector).createDataSet(MockClass, 'id')
|
|
38
|
+
getRepository(injector).createDataSet(User, 'username')
|
|
39
|
+
getRepository(injector).createDataSet(DefaultSession, 'sessionId')
|
|
35
40
|
|
|
36
41
|
const api = await useRestService<ValidationApi>({
|
|
37
42
|
injector,
|