@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.
- package/CHANGELOG.md +177 -0
- package/dist/actions/index.js +6 -6
- package/dist/actions/index.js.map +1 -1
- package/dist/api-manager.js +2 -2
- package/dist/api-manager.js.map +1 -1
- package/dist/endpoint-generators/create-delete-endpoint.spec.js +1 -1
- package/dist/endpoint-generators/create-delete-endpoint.spec.js.map +1 -1
- package/dist/endpoint-generators/create-get-collection-endpoint.spec.js +1 -1
- package/dist/endpoint-generators/create-get-collection-endpoint.spec.js.map +1 -1
- package/dist/endpoint-generators/create-get-entity-endpoint.spec.js +1 -1
- package/dist/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -1
- package/dist/endpoint-generators/create-patch-endpoint.spec.js +1 -1
- package/dist/endpoint-generators/create-patch-endpoint.spec.js.map +1 -1
- package/dist/endpoint-generators/create-post-endpoint.spec.js +1 -1
- package/dist/endpoint-generators/create-post-endpoint.spec.js.map +1 -1
- package/dist/endpoint-generators/index.js +5 -5
- package/dist/endpoint-generators/index.js.map +1 -1
- package/dist/http-authentication-settings.d.ts +1 -4
- package/dist/http-authentication-settings.d.ts.map +1 -1
- package/dist/http-authentication-settings.js +1 -3
- package/dist/http-authentication-settings.js.map +1 -1
- package/dist/http-user-context.d.ts +6 -8
- package/dist/http-user-context.d.ts.map +1 -1
- package/dist/http-user-context.js +27 -28
- package/dist/http-user-context.js.map +1 -1
- package/dist/http-user-context.spec.d.ts.map +1 -1
- package/dist/http-user-context.spec.js +33 -26
- package/dist/http-user-context.spec.js.map +1 -1
- package/dist/incoming-message-extensions.js +1 -1
- package/dist/incoming-message-extensions.js.map +1 -1
- package/dist/index.js +15 -15
- package/dist/index.js.map +1 -1
- package/dist/models/index.js +2 -2
- package/dist/models/index.js.map +1 -1
- package/dist/rest-service.integration.spec.js +1 -1
- package/dist/rest-service.integration.spec.js.map +1 -1
- package/dist/schema-validator/index.js +2 -2
- package/dist/schema-validator/index.js.map +1 -1
- package/dist/schema-validator/schema-validator.js +2 -2
- package/dist/schema-validator/schema-validator.js.map +1 -1
- package/dist/server-manager.js +2 -2
- package/dist/server-manager.js.map +1 -1
- package/dist/server-response-extensions.js +1 -1
- package/dist/server-response-extensions.js.map +1 -1
- package/dist/utils.js +1 -1
- package/dist/validate.integration.spec.js +1 -1
- package/dist/validate.integration.spec.js.map +1 -1
- package/dist/validate.js.map +1 -1
- package/package.json +14 -14
- package/src/http-authentication-settings.ts +2 -5
- package/src/http-user-context.spec.ts +44 -26
- 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.
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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` },
|
package/src/http-user-context.ts
CHANGED
|
@@ -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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
116
|
+
const session = await this.getSessionStore().get(sessionId)
|
|
112
117
|
if (session) {
|
|
113
|
-
const
|
|
114
|
-
|
|
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
|
|
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
|
}
|