@furystack/rest-service 4.1.10 → 5.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 (52) hide show
  1. package/CHANGELOG.md +177 -0
  2. package/dist/actions/index.js +6 -6
  3. package/dist/actions/index.js.map +1 -1
  4. package/dist/api-manager.js +2 -2
  5. package/dist/api-manager.js.map +1 -1
  6. package/dist/endpoint-generators/create-delete-endpoint.spec.js +1 -1
  7. package/dist/endpoint-generators/create-delete-endpoint.spec.js.map +1 -1
  8. package/dist/endpoint-generators/create-get-collection-endpoint.spec.js +1 -1
  9. package/dist/endpoint-generators/create-get-collection-endpoint.spec.js.map +1 -1
  10. package/dist/endpoint-generators/create-get-entity-endpoint.spec.js +1 -1
  11. package/dist/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -1
  12. package/dist/endpoint-generators/create-patch-endpoint.spec.js +1 -1
  13. package/dist/endpoint-generators/create-patch-endpoint.spec.js.map +1 -1
  14. package/dist/endpoint-generators/create-post-endpoint.spec.js +1 -1
  15. package/dist/endpoint-generators/create-post-endpoint.spec.js.map +1 -1
  16. package/dist/endpoint-generators/index.js +5 -5
  17. package/dist/endpoint-generators/index.js.map +1 -1
  18. package/dist/http-authentication-settings.d.ts +1 -4
  19. package/dist/http-authentication-settings.d.ts.map +1 -1
  20. package/dist/http-authentication-settings.js +1 -3
  21. package/dist/http-authentication-settings.js.map +1 -1
  22. package/dist/http-user-context.d.ts +6 -8
  23. package/dist/http-user-context.d.ts.map +1 -1
  24. package/dist/http-user-context.js +27 -28
  25. package/dist/http-user-context.js.map +1 -1
  26. package/dist/http-user-context.spec.d.ts.map +1 -1
  27. package/dist/http-user-context.spec.js +33 -26
  28. package/dist/http-user-context.spec.js.map +1 -1
  29. package/dist/incoming-message-extensions.js +1 -1
  30. package/dist/incoming-message-extensions.js.map +1 -1
  31. package/dist/index.js +15 -15
  32. package/dist/index.js.map +1 -1
  33. package/dist/models/index.js +2 -2
  34. package/dist/models/index.js.map +1 -1
  35. package/dist/rest-service.integration.spec.js +1 -1
  36. package/dist/rest-service.integration.spec.js.map +1 -1
  37. package/dist/schema-validator/index.js +2 -2
  38. package/dist/schema-validator/index.js.map +1 -1
  39. package/dist/schema-validator/schema-validator.js +2 -2
  40. package/dist/schema-validator/schema-validator.js.map +1 -1
  41. package/dist/server-manager.js +2 -2
  42. package/dist/server-manager.js.map +1 -1
  43. package/dist/server-response-extensions.js +1 -1
  44. package/dist/server-response-extensions.js.map +1 -1
  45. package/dist/utils.js +1 -1
  46. package/dist/validate.integration.spec.js +1 -1
  47. package/dist/validate.integration.spec.js.map +1 -1
  48. package/dist/validate.js.map +1 -1
  49. package/package.json +14 -14
  50. package/src/http-authentication-settings.ts +2 -5
  51. package/src/http-user-context.spec.ts +44 -26
  52. package/src/http-user-context.ts +24 -25
@@ -5,18 +5,29 @@ import { User, StoreManager, InMemoryStore } from '@furystack/core'
5
5
  import { DefaultSession } from './models/default-session'
6
6
  import { HttpUserContext } from './http-user-context'
7
7
  import './injector-extensions'
8
+ import { PasswordAuthenticator, PasswordCredential, UnauthenticatedError } from '@furystack/security'
8
9
 
9
10
  export const prepareInjector = async (i: Injector) => {
10
11
  i.setupStores((sm) =>
11
12
  sm
12
13
  .addStore(new InMemoryStore({ model: User, primaryKey: 'username' }))
13
- .addStore(new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' })),
14
+ .addStore(new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }))
15
+ .addStore(new InMemoryStore({ model: PasswordCredential, primaryKey: 'userName' })),
14
16
  )
15
17
 
16
18
  i.useHttpAuthentication()
17
19
  // await i.getInstance(ServerManager).getOrCreate({ port: 19999 })
18
20
  }
19
21
 
22
+ const setupUser = async (i: Injector, userName: string, password: string) => {
23
+ const sm = i.getInstance(StoreManager)
24
+ const pw = i.getInstance(PasswordAuthenticator)
25
+ const hasher = pw.getHasher()
26
+ const cred = await hasher.createCredential(userName, password)
27
+ await sm.getStoreFor(PasswordCredential, 'userName').add(cred)
28
+ await sm.getStoreFor(User, 'username').add({ username: userName, roles: [] })
29
+ }
30
+
20
31
  describe('HttpUserContext', () => {
21
32
  const request = { headers: {} } as IncomingMessage
22
33
  const response = {} as any as ServerResponse
@@ -85,52 +96,58 @@ describe('HttpUserContext', () => {
85
96
  await usingAsync(new Injector(), async (i) => {
86
97
  await prepareInjector(i)
87
98
  const ctx = i.getInstance(HttpUserContext)
88
- await expect(ctx.authenticateUser('user', 'password')).rejects.toThrow('')
99
+ await expect(ctx.authenticateUser('user', 'password')).rejects.toThrowError(UnauthenticatedError)
89
100
  })
90
101
  })
91
102
 
92
103
  it('Should fail when the password not equals', async () => {
93
104
  await usingAsync(new Injector(), async (i) => {
94
105
  await prepareInjector(i)
95
- const ctx = i.getInstance(HttpUserContext)
96
- ctx.authentication
97
- .getUserStore(i.getInstance(StoreManager))
98
- .add({ username: 'user', password: ctx.authentication.hashMethod('pass123'), roles: [] })
99
- await expect(ctx.authenticateUser('user', 'pass321')).rejects.toThrow('')
106
+ await setupUser(i, 'user', 'pass123')
107
+ await expect(i.getInstance(HttpUserContext).authenticateUser('user', 'pass321')).rejects.toThrowError(
108
+ UnauthenticatedError,
109
+ )
100
110
  })
101
111
  })
102
112
 
103
113
  it('Should fail when the username not equals', async () => {
104
114
  await usingAsync(new Injector(), async (i) => {
105
115
  await prepareInjector(i)
106
- const ctx = i.getInstance(HttpUserContext)
107
- ctx.authentication
108
- .getUserStore(i.getInstance(StoreManager))
109
- .add({ username: 'otherUser', password: ctx.authentication.hashMethod('pass123'), roles: [] })
110
- expect(ctx.authenticateUser('user', 'pass123')).rejects.toThrow('')
116
+ await setupUser(i, 'otherUser', 'pass123')
117
+ expect(i.getInstance(HttpUserContext).authenticateUser('user', 'pass123')).rejects.toThrowError(
118
+ UnauthenticatedError,
119
+ )
111
120
  })
112
121
  })
113
122
 
114
123
  it('Should fail when password not provided', async () => {
115
124
  await usingAsync(new Injector(), async (i) => {
116
125
  await prepareInjector(i)
117
- const ctx = i.getInstance(HttpUserContext)
118
- ctx.authentication
119
- .getUserStore(i.getInstance(StoreManager))
120
- .add({ username: 'otherUser', password: ctx.authentication.hashMethod('pass123'), roles: [] })
121
- await expect(ctx.authenticateUser('user', '')).rejects.toThrow('')
126
+ await setupUser(i, 'user', 'pass123')
127
+ await expect(i.getInstance(HttpUserContext).authenticateUser('user', '')).rejects.toThrowError(
128
+ UnauthenticatedError,
129
+ )
122
130
  })
123
131
  })
124
132
 
125
- it('Should return the user without the password hash when the username and password matches', async () => {
133
+ it('Should fail when the user is not in the user store', async () => {
126
134
  await usingAsync(new Injector(), async (i) => {
127
135
  await prepareInjector(i)
136
+ await setupUser(i, 'user', 'pass123')
137
+ await i.getInstance(StoreManager).getStoreFor(User, 'username').remove('user')
138
+ await expect(i.getInstance(HttpUserContext).authenticateUser('user', 'pass123')).rejects.toThrowError(
139
+ UnauthenticatedError,
140
+ )
141
+ })
142
+ })
143
+
144
+ it('Should return the user when the username and password matches', async () => {
145
+ await usingAsync(new Injector(), async (i) => {
146
+ await prepareInjector(i)
147
+ await setupUser(i, 'user', 'pass123')
128
148
  const ctx = i.getInstance(HttpUserContext)
129
- const store = ctx.authentication.getUserStore(i.getInstance(StoreManager))
130
- const loginUser = { username: 'user', roles: [] }
131
- store.add({ ...loginUser, password: ctx.authentication.hashMethod('pass123') })
132
149
  const value = await ctx.authenticateUser('user', 'pass123')
133
- expect(value).toEqual(loginUser)
150
+ expect(value).toEqual({ username: 'user', roles: [] })
134
151
  })
135
152
  })
136
153
  })
@@ -193,7 +210,7 @@ describe('HttpUserContext', () => {
193
210
  ctx.authenticateRequest({
194
211
  headers: { authorization: `Basic dGVzdHVzZXI6cGFzc3dvcmQ=` },
195
212
  } as IncomingMessage),
196
- ).rejects.toThrow('')
213
+ ).rejects.toThrowError(UnauthenticatedError)
197
214
  expect(ctx.authenticateUser).not.toBeCalled()
198
215
  })
199
216
  })
@@ -206,7 +223,7 @@ describe('HttpUserContext', () => {
206
223
  ctx.authenticateRequest({
207
224
  headers: { cookie: `${ctx.authentication.cookieName}=666;a=3` },
208
225
  } as IncomingMessage),
209
- ).rejects.toThrow('')
226
+ ).rejects.toThrowError(UnauthenticatedError)
210
227
  })
211
228
  })
212
229
 
@@ -221,19 +238,20 @@ describe('HttpUserContext', () => {
221
238
  ctx.authenticateRequest({
222
239
  headers: { cookie: `${ctx.authentication.cookieName}=666;a=3` },
223
240
  } as IncomingMessage),
224
- ).rejects.toThrow('')
241
+ ).rejects.toThrowError(UnauthenticatedError)
225
242
  })
226
243
  })
227
244
 
228
245
  it('Should authenticate with cookie, if the session IDs matches', async () => {
229
246
  await usingAsync(new Injector(), async (i) => {
230
247
  await prepareInjector(i)
248
+
231
249
  const ctx = i.getInstance(HttpUserContext)
232
250
  ctx.authentication
233
251
  .getSessionStore(i.getInstance(StoreManager))
234
252
  .add({ sessionId: '666', username: testUser.username })
235
253
 
236
- ctx.authentication.getUserStore(i.getInstance(StoreManager)).add({ ...testUser, password: '' })
254
+ ctx.authentication.getUserStore(i.getInstance(StoreManager)).add({ ...testUser })
237
255
 
238
256
  const result = await ctx.authenticateRequest({
239
257
  headers: { cookie: `${ctx.authentication.cookieName}=666;a=3` },
@@ -4,6 +4,7 @@ import { Injectable } from '@furystack/inject'
4
4
  import { v1 } from 'uuid'
5
5
  import { HttpAuthenticationSettings } from './http-authentication-settings'
6
6
  import { DefaultSession } from 'models/default-session'
7
+ import { PasswordAuthenticator, UnauthenticatedError } from '@furystack/security'
7
8
 
8
9
  /**
9
10
  * Injectable UserContext for FuryStack HTTP Api
@@ -14,6 +15,15 @@ export class HttpUserContext {
14
15
 
15
16
  public getSessionStore = () => this.authentication.getSessionStore(this.storeManager)
16
17
 
18
+ private getUserByName = async (userName: string) => {
19
+ const userStore = this.getUserStore()
20
+ const users = await userStore.find({ filter: { username: { $eq: userName } }, top: 2 })
21
+ if (users.length !== 1) {
22
+ throw new UnauthenticatedError()
23
+ }
24
+ return users[0]
25
+ }
26
+
17
27
  private user?: User
18
28
 
19
29
  /**
@@ -54,21 +64,16 @@ export class HttpUserContext {
54
64
  * @returns the authenticated User
55
65
  */
56
66
  public async authenticateUser(userName: string, password: string) {
57
- const match =
58
- (password &&
59
- password.length &&
60
- (await this.getUserStore().find({
61
- filter: {
62
- username: { $eq: userName },
63
- password: { $eq: this.authentication.hashMethod(password) },
64
- },
65
- }))) ||
66
- []
67
- if (match.length === 1) {
68
- const { password: pw, ...user } = match[0]
69
- return user
67
+ const result = await this.authenticator.checkPasswordForUser(userName, password)
68
+
69
+ if (!result.isValid) {
70
+ throw new UnauthenticatedError()
70
71
  }
71
- throw Error('Failed to authenticate.')
72
+ const user = await this.getUserByName(userName)
73
+ if (!user) {
74
+ throw new UnauthenticatedError()
75
+ }
76
+ return user
72
77
  }
73
78
 
74
79
  public async getCurrentUser(request: IncomingMessage) {
@@ -108,23 +113,16 @@ export class HttpUserContext {
108
113
  // Cookie auth
109
114
  const sessionId = this.getSessionIdFromRequest(request)
110
115
  if (sessionId) {
111
- const [session] = await this.getSessionStore().find({ filter: { sessionId: { $eq: sessionId } }, top: 2 })
116
+ const session = await this.getSessionStore().get(sessionId)
112
117
  if (session) {
113
- const userResult = await this.getUserStore().find({
114
- filter: {
115
- username: { $eq: session.username },
116
- },
117
- top: 2,
118
- })
119
- if (userResult.length === 1) {
120
- const { password, ...user } = userResult[0]
118
+ const user = await this.getUserByName(session.username)
119
+ if (user) {
121
120
  return user
122
121
  }
123
- throw Error('Inconsistent session result')
124
122
  }
125
123
  }
126
124
 
127
- throw Error('Failed to authenticate request')
125
+ throw new UnauthenticatedError()
128
126
  }
129
127
 
130
128
  /**
@@ -156,5 +154,6 @@ export class HttpUserContext {
156
154
  constructor(
157
155
  public readonly authentication: HttpAuthenticationSettings<User, DefaultSession>,
158
156
  private readonly storeManager: StoreManager,
157
+ private readonly authenticator: PasswordAuthenticator,
159
158
  ) {}
160
159
  }