@furystack/rest-service 12.3.5 → 14.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 +55 -0
- package/README.md +47 -28
- package/esm/actions/get-current-user.spec.js +10 -6
- package/esm/actions/get-current-user.spec.js.map +1 -1
- package/esm/actions/index.d.ts +0 -1
- package/esm/actions/index.d.ts.map +1 -1
- package/esm/actions/index.js +0 -1
- package/esm/actions/index.js.map +1 -1
- package/esm/actions/is-authenticated.spec.js +10 -6
- package/esm/actions/is-authenticated.spec.js.map +1 -1
- package/esm/actions/logout-action.spec.js +11 -9
- package/esm/actions/logout-action.spec.js.map +1 -1
- package/esm/actions/logout.js +1 -1
- package/esm/actions/logout.js.map +1 -1
- package/esm/actions/password-login-action.js +1 -1
- package/esm/actions/password-login-action.js.map +1 -1
- package/esm/actions/password-login-action.spec.js +23 -28
- package/esm/actions/password-login-action.spec.js.map +1 -1
- package/esm/authenticate.js +1 -1
- package/esm/authenticate.js.map +1 -1
- package/esm/authenticate.spec.js +41 -33
- package/esm/authenticate.spec.js.map +1 -1
- package/esm/authorize.spec.js +34 -29
- package/esm/authorize.spec.js.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.d.ts +6 -11
- package/esm/endpoint-generators/create-delete-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.js +7 -11
- package/esm/endpoint-generators/create-delete-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-delete-endpoint.spec.js +14 -21
- package/esm/endpoint-generators/create-delete-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.d.ts +11 -11
- package/esm/endpoint-generators/create-get-collection-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.js +13 -12
- package/esm/endpoint-generators/create-get-collection-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-get-collection-endpoint.spec.js +43 -70
- package/esm/endpoint-generators/create-get-collection-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.d.ts +6 -11
- package/esm/endpoint-generators/create-get-entity-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.js +7 -11
- package/esm/endpoint-generators/create-get-entity-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-get-entity-endpoint.spec.js +16 -33
- package/esm/endpoint-generators/create-get-entity-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.d.ts +1 -15
- package/esm/endpoint-generators/create-get-openapi-document-action.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-openapi-document-action.js +1 -24
- package/esm/endpoint-generators/create-get-openapi-document-action.js.map +1 -1
- package/esm/endpoint-generators/create-get-schema-action.d.ts +1 -1
- package/esm/endpoint-generators/create-get-schema-action.d.ts.map +1 -1
- package/esm/endpoint-generators/create-get-schema-action.js.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.d.ts +7 -13
- package/esm/endpoint-generators/create-patch-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.js +8 -13
- package/esm/endpoint-generators/create-patch-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-patch-endpoint.spec.js +10 -17
- package/esm/endpoint-generators/create-patch-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.d.ts +7 -13
- package/esm/endpoint-generators/create-post-endpoint.d.ts.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.js +8 -13
- package/esm/endpoint-generators/create-post-endpoint.js.map +1 -1
- package/esm/endpoint-generators/create-post-endpoint.spec.js +41 -14
- package/esm/endpoint-generators/create-post-endpoint.spec.js.map +1 -1
- package/esm/endpoint-generators/utils.d.ts +17 -2
- package/esm/endpoint-generators/utils.d.ts.map +1 -1
- package/esm/endpoint-generators/utils.js +33 -20
- package/esm/endpoint-generators/utils.js.map +1 -1
- package/esm/endpoint-generators/with-schema-and-openapi-action.d.ts +0 -2
- package/esm/endpoint-generators/with-schema-and-openapi-action.d.ts.map +1 -1
- package/esm/get-schema-from-api.d.ts +1 -1
- package/esm/get-schema-from-api.d.ts.map +1 -1
- package/esm/get-schema-from-api.spec.js.map +1 -1
- package/esm/helpers.d.ts +53 -87
- package/esm/helpers.d.ts.map +1 -1
- package/esm/helpers.js +91 -92
- package/esm/helpers.js.map +1 -1
- package/esm/helpers.spec.d.ts +2 -1
- package/esm/helpers.spec.d.ts.map +1 -1
- package/esm/helpers.spec.js +41 -38
- package/esm/helpers.spec.js.map +1 -1
- package/esm/http-authentication-settings.d.ts +35 -12
- package/esm/http-authentication-settings.d.ts.map +1 -1
- package/esm/http-authentication-settings.js +21 -29
- package/esm/http-authentication-settings.js.map +1 -1
- package/esm/http-server-pool.d.ts +64 -0
- package/esm/http-server-pool.d.ts.map +1 -0
- package/esm/http-server-pool.js +95 -0
- package/esm/http-server-pool.js.map +1 -0
- package/esm/http-server-pool.spec.d.ts +2 -0
- package/esm/http-server-pool.spec.d.ts.map +1 -0
- package/esm/http-server-pool.spec.js +193 -0
- package/esm/http-server-pool.spec.js.map +1 -0
- package/esm/http-user-context.d.ts +31 -44
- package/esm/http-user-context.d.ts.map +1 -1
- package/esm/http-user-context.js +76 -95
- package/esm/http-user-context.js.map +1 -1
- package/esm/http-user-context.spec.d.ts +8 -2
- package/esm/http-user-context.spec.d.ts.map +1 -1
- package/esm/http-user-context.spec.js +189 -337
- package/esm/http-user-context.spec.js.map +1 -1
- package/esm/index.d.ts +6 -4
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +6 -4
- package/esm/index.js.map +1 -1
- package/esm/login-response-strategy.d.ts +11 -10
- package/esm/login-response-strategy.d.ts.map +1 -1
- package/esm/login-response-strategy.js +7 -6
- package/esm/login-response-strategy.js.map +1 -1
- package/esm/login-response-strategy.spec.js +25 -29
- package/esm/login-response-strategy.spec.js.map +1 -1
- package/esm/openapi/generate-openapi-document.d.ts +0 -8
- package/esm/openapi/generate-openapi-document.d.ts.map +1 -1
- package/esm/openapi/generate-openapi-document.js +0 -2
- package/esm/openapi/generate-openapi-document.js.map +1 -1
- package/esm/proxy-manager.spec.js +152 -176
- package/esm/proxy-manager.spec.js.map +1 -1
- package/esm/proxy-runtime.d.ts +27 -0
- package/esm/proxy-runtime.d.ts.map +1 -0
- package/esm/proxy-runtime.js +45 -0
- package/esm/proxy-runtime.js.map +1 -0
- package/esm/proxy-runtime.spec.d.ts +2 -0
- package/esm/proxy-runtime.spec.d.ts.map +1 -0
- package/esm/proxy-runtime.spec.js +39 -0
- package/esm/proxy-runtime.spec.js.map +1 -0
- package/esm/rest-api-runtime.d.ts +76 -0
- package/esm/rest-api-runtime.d.ts.map +1 -0
- package/esm/rest-api-runtime.js +131 -0
- package/esm/rest-api-runtime.js.map +1 -0
- package/esm/rest-api-runtime.spec.d.ts +2 -0
- package/esm/rest-api-runtime.spec.d.ts.map +1 -0
- package/esm/rest-api-runtime.spec.js +58 -0
- package/esm/rest-api-runtime.spec.js.map +1 -0
- package/esm/rest-service.integration.spec.d.ts +1 -1
- package/esm/rest-service.integration.spec.d.ts.map +1 -1
- package/esm/rest-service.integration.spec.js +60 -82
- package/esm/rest-service.integration.spec.js.map +1 -1
- package/esm/rest.integration.test.js +26 -84
- package/esm/rest.integration.test.js.map +1 -1
- package/esm/server-telemetry.d.ts +57 -0
- package/esm/server-telemetry.d.ts.map +1 -0
- package/esm/server-telemetry.js +26 -0
- package/esm/server-telemetry.js.map +1 -0
- package/esm/server-telemetry.spec.d.ts +2 -0
- package/esm/server-telemetry.spec.d.ts.map +1 -0
- package/esm/server-telemetry.spec.js +52 -0
- package/esm/server-telemetry.spec.js.map +1 -0
- package/esm/static-server-manager.spec.js +60 -125
- package/esm/static-server-manager.spec.js.map +1 -1
- package/esm/static-site-runtime.d.ts +21 -0
- package/esm/static-site-runtime.d.ts.map +1 -0
- package/esm/static-site-runtime.js +48 -0
- package/esm/static-site-runtime.js.map +1 -0
- package/esm/static-site-runtime.spec.d.ts +2 -0
- package/esm/static-site-runtime.spec.d.ts.map +1 -0
- package/esm/static-site-runtime.spec.js +30 -0
- package/esm/static-site-runtime.spec.js.map +1 -0
- package/esm/user-store.d.ts +39 -0
- package/esm/user-store.d.ts.map +1 -0
- package/esm/user-store.js +62 -0
- package/esm/user-store.js.map +1 -0
- package/esm/user-store.spec.d.ts +2 -0
- package/esm/user-store.spec.d.ts.map +1 -0
- package/esm/user-store.spec.js +72 -0
- package/esm/user-store.spec.js.map +1 -0
- package/esm/validate.integration.spec.js +26 -83
- package/esm/validate.integration.spec.js.map +1 -1
- package/package.json +8 -8
- package/src/actions/get-current-user.spec.ts +11 -10
- package/src/actions/index.ts +0 -1
- package/src/actions/is-authenticated.spec.ts +12 -7
- package/src/actions/logout-action.spec.ts +12 -12
- package/src/actions/logout.ts +1 -1
- package/src/actions/password-login-action.spec.ts +30 -30
- package/src/actions/password-login-action.ts +1 -1
- package/src/authenticate.spec.ts +45 -51
- package/src/authenticate.ts +1 -1
- package/src/authorize.spec.ts +37 -39
- package/src/endpoint-generators/create-delete-endpoint.spec.ts +16 -24
- package/src/endpoint-generators/create-delete-endpoint.ts +11 -17
- package/src/endpoint-generators/create-get-collection-endpoint.spec.ts +45 -78
- package/src/endpoint-generators/create-get-collection-endpoint.ts +17 -19
- package/src/endpoint-generators/create-get-entity-endpoint.spec.ts +17 -33
- package/src/endpoint-generators/create-get-entity-endpoint.ts +11 -17
- package/src/endpoint-generators/create-get-openapi-document-action.ts +2 -38
- package/src/endpoint-generators/create-get-schema-action.ts +1 -1
- package/src/endpoint-generators/create-patch-endpoint.spec.ts +11 -18
- package/src/endpoint-generators/create-patch-endpoint.ts +12 -23
- package/src/endpoint-generators/create-post-endpoint.spec.ts +45 -14
- package/src/endpoint-generators/create-post-endpoint.ts +13 -23
- package/src/endpoint-generators/utils.ts +36 -28
- package/src/endpoint-generators/with-schema-and-openapi-action.ts +0 -3
- package/src/get-schema-from-api.spec.ts +1 -1
- package/src/get-schema-from-api.ts +1 -1
- package/src/helpers.spec.ts +51 -41
- package/src/helpers.ts +140 -109
- package/src/http-authentication-settings.ts +51 -25
- package/src/http-server-pool.spec.ts +220 -0
- package/src/http-server-pool.ts +169 -0
- package/src/http-user-context.spec.ts +203 -383
- package/src/http-user-context.ts +114 -91
- package/src/index.ts +6 -4
- package/src/login-response-strategy.spec.ts +32 -31
- package/src/login-response-strategy.ts +13 -12
- package/src/openapi/generate-openapi-document.ts +0 -3
- package/src/proxy-manager.spec.ts +152 -182
- package/src/proxy-runtime.spec.ts +50 -0
- package/src/proxy-runtime.ts +75 -0
- package/src/rest-api-runtime.spec.ts +77 -0
- package/src/rest-api-runtime.ts +204 -0
- package/src/rest-service.integration.spec.ts +68 -87
- package/src/rest.integration.test.ts +27 -87
- package/src/server-telemetry.spec.ts +56 -0
- package/src/server-telemetry.ts +51 -0
- package/src/static-server-manager.spec.ts +63 -151
- package/src/static-site-runtime.spec.ts +39 -0
- package/src/static-site-runtime.ts +75 -0
- package/src/user-store.spec.ts +86 -0
- package/src/user-store.ts +68 -0
- package/src/validate.integration.spec.ts +39 -97
- package/esm/actions/login-action.spec.d.ts +0 -2
- package/esm/actions/login-action.spec.d.ts.map +0 -1
- package/esm/actions/login-action.spec.js +0 -34
- package/esm/actions/login-action.spec.js.map +0 -1
- package/esm/actions/login.d.ts +0 -19
- package/esm/actions/login.d.ts.map +0 -1
- package/esm/actions/login.js +0 -27
- package/esm/actions/login.js.map +0 -1
- package/esm/api-manager.d.ts +0 -104
- package/esm/api-manager.d.ts.map +0 -1
- package/esm/api-manager.js +0 -180
- package/esm/api-manager.js.map +0 -1
- package/esm/proxy-manager.d.ts +0 -53
- package/esm/proxy-manager.d.ts.map +0 -1
- package/esm/proxy-manager.js +0 -84
- package/esm/proxy-manager.js.map +0 -1
- package/esm/server-manager.d.ts +0 -48
- package/esm/server-manager.d.ts.map +0 -1
- package/esm/server-manager.js +0 -100
- package/esm/server-manager.js.map +0 -1
- package/esm/static-server-manager.d.ts +0 -19
- package/esm/static-server-manager.d.ts.map +0 -1
- package/esm/static-server-manager.js +0 -65
- package/esm/static-server-manager.js.map +0 -1
- package/esm/swagger/generate-swagger-json.spec.d.ts +0 -2
- package/esm/swagger/generate-swagger-json.spec.d.ts.map +0 -1
- package/esm/swagger/generate-swagger-json.spec.js +0 -215
- package/esm/swagger/generate-swagger-json.spec.js.map +0 -1
- package/src/actions/login-action.spec.ts +0 -42
- package/src/actions/login.ts +0 -32
- package/src/api-manager.ts +0 -308
- package/src/proxy-manager.ts +0 -102
- package/src/server-manager.ts +0 -135
- package/src/static-server-manager.ts +0 -90
- package/src/swagger/generate-swagger-json.spec.ts +0 -250
package/src/http-user-context.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import type { User } from '@furystack/core'
|
|
2
2
|
import { useSystemIdentityContext } from '@furystack/core'
|
|
3
3
|
import type { Injector } from '@furystack/inject'
|
|
4
|
-
import {
|
|
4
|
+
import { defineService, type Token } from '@furystack/inject'
|
|
5
5
|
import { PasswordAuthenticator, UnauthenticatedError } from '@furystack/security'
|
|
6
6
|
import { EventHub, type ListenerErrorPayload } from '@furystack/utils'
|
|
7
7
|
import { randomBytes } from 'crypto'
|
|
8
8
|
import type { IncomingMessage } from 'http'
|
|
9
9
|
import { extractSessionIdFromCookies } from './authentication-providers/helpers.js'
|
|
10
10
|
import { HttpAuthenticationSettings } from './http-authentication-settings.js'
|
|
11
|
-
import type { DefaultSession } from './models/default-session.js'
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
|
-
* Events emitted by
|
|
13
|
+
* Events emitted by a per-request {@link HttpUserContext} instance. Since
|
|
14
|
+
* the context is scoped, each request sees its own event stream.
|
|
15
15
|
*/
|
|
16
16
|
export type HttpUserContextEvents = {
|
|
17
17
|
onLogin: { user: User }
|
|
@@ -21,45 +21,77 @@ export type HttpUserContextEvents = {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* Request-scoped authentication/authorization facade.
|
|
25
|
+
*
|
|
26
|
+
* `HttpUserContext` consolidates identity lookup, authentication-provider
|
|
27
|
+
* walk-through and cookie session helpers behind a single object. Resolved
|
|
28
|
+
* users are memoized per `request.headers` via a {@link WeakMap} so repeated
|
|
29
|
+
* calls within the same request skip re-authentication — and so ancestor
|
|
30
|
+
* scopes caching the same `HttpUserContext` instance cannot leak an
|
|
31
|
+
* authenticated user into sibling requests.
|
|
25
32
|
*/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
export interface HttpUserContext extends EventHub<HttpUserContextEvents> {
|
|
34
|
+
/** The active {@link HttpAuthenticationSettings} captured at resolve time. */
|
|
35
|
+
readonly authentication: HttpAuthenticationSettings
|
|
36
|
+
/** Whether the current user is authenticated according to the bound providers. */
|
|
37
|
+
isAuthenticated(request: Pick<IncomingMessage, 'headers'>): Promise<boolean>
|
|
38
|
+
/** Returns `true` when the current user has **all** of the supplied roles. */
|
|
39
|
+
isAuthorized(request: Pick<IncomingMessage, 'headers'>, ...roles: string[]): Promise<boolean>
|
|
40
|
+
/** Verifies a username/password pair against the system `PasswordAuthenticator`. */
|
|
41
|
+
authenticateUser(userName: string, password: string): Promise<User>
|
|
42
|
+
/** Returns the cached user for this request or resolves it via the provider chain. */
|
|
43
|
+
getCurrentUser(request: Pick<IncomingMessage, 'headers'>): Promise<User>
|
|
44
|
+
/** Extracts the session id for `request` using the configured cookie name. */
|
|
45
|
+
getSessionIdFromRequest(request: Pick<IncomingMessage, 'headers'>): string | null
|
|
46
|
+
/** Walks the configured authentication providers and returns the first match. */
|
|
47
|
+
authenticateRequest(request: Pick<IncomingMessage, 'headers'>): Promise<User>
|
|
48
|
+
/** Creates and persists a session for `user`, setting the cookie on `response`. */
|
|
49
|
+
cookieLogin(user: User, response: { setHeader: (header: string, value: string) => void }): Promise<User>
|
|
50
|
+
/** Clears the session identified by the request's cookie (if any). */
|
|
51
|
+
cookieLogout(
|
|
52
|
+
request: Pick<IncomingMessage, 'headers'>,
|
|
53
|
+
response: { setHeader: (header: string, value: string) => void },
|
|
54
|
+
): Promise<void>
|
|
55
|
+
}
|
|
40
56
|
|
|
41
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Internal concrete EventHub-backed {@link HttpUserContext}. Exposed via
|
|
59
|
+
* the {@link HttpUserContextToken} token; constructed by its factory.
|
|
60
|
+
*/
|
|
61
|
+
class HttpUserContextImpl extends EventHub<HttpUserContextEvents> implements HttpUserContext {
|
|
62
|
+
/**
|
|
63
|
+
* Per-request user cache keyed by the request's `headers` object identity.
|
|
64
|
+
* Each incoming HTTP/WS message allocates its own `headers` object, so
|
|
65
|
+
* entries here never leak between requests — even when this
|
|
66
|
+
* `HttpUserContext` instance happens to be cached by an ancestor scope
|
|
67
|
+
* (e.g. because setup code resolved it on the root injector before any
|
|
68
|
+
* per-request child scope existed).
|
|
69
|
+
*/
|
|
70
|
+
private readonly userCache = new WeakMap<object, Promise<User>>()
|
|
42
71
|
|
|
43
72
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
73
|
+
* `authenticator` is resolved lazily so that tests (and applications) that
|
|
74
|
+
* never invoke `authenticateUser` don't need to bind password-credential
|
|
75
|
+
* stores.
|
|
46
76
|
*/
|
|
47
|
-
|
|
77
|
+
constructor(
|
|
78
|
+
public readonly authentication: HttpAuthenticationSettings,
|
|
79
|
+
private readonly resolveAuthenticator: () => PasswordAuthenticator,
|
|
80
|
+
private readonly systemInjector: Injector,
|
|
81
|
+
) {
|
|
82
|
+
super()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public async isAuthenticated(request: Pick<IncomingMessage, 'headers'>): Promise<boolean> {
|
|
48
86
|
try {
|
|
49
87
|
const currentUser = await this.getCurrentUser(request)
|
|
50
88
|
return currentUser !== null
|
|
51
|
-
} catch
|
|
89
|
+
} catch {
|
|
52
90
|
return false
|
|
53
91
|
}
|
|
54
92
|
}
|
|
55
93
|
|
|
56
|
-
|
|
57
|
-
* Returns whether the current user can be authorized with ALL of the specified roles
|
|
58
|
-
* @param request The request to be authenticated
|
|
59
|
-
* @param roles The list of roles to authorize
|
|
60
|
-
* @returns a boolean value that indicates whether the user is authorized
|
|
61
|
-
*/
|
|
62
|
-
public async isAuthorized(request: IncomingMessage, ...roles: string[]): Promise<boolean> {
|
|
94
|
+
public async isAuthorized(request: Pick<IncomingMessage, 'headers'>, ...roles: string[]): Promise<boolean> {
|
|
63
95
|
const currentUser = await this.getCurrentUser(request)
|
|
64
96
|
for (const role of roles) {
|
|
65
97
|
if (!currentUser || !currentUser.roles.some((c) => c === role)) {
|
|
@@ -69,15 +101,8 @@ export class HttpUserContext extends EventHub<HttpUserContextEvents> {
|
|
|
69
101
|
return true
|
|
70
102
|
}
|
|
71
103
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
* @param userName The username
|
|
75
|
-
* @param password The password
|
|
76
|
-
* @returns the authenticated User
|
|
77
|
-
*/
|
|
78
|
-
public async authenticateUser(userName: string, password: string) {
|
|
79
|
-
const result = await this.authenticator.checkPasswordForUser(userName, password)
|
|
80
|
-
|
|
104
|
+
public async authenticateUser(userName: string, password: string): Promise<User> {
|
|
105
|
+
const result = await this.resolveAuthenticator().checkPasswordForUser(userName, password)
|
|
81
106
|
if (!result.isValid) {
|
|
82
107
|
throw new UnauthenticatedError()
|
|
83
108
|
}
|
|
@@ -88,24 +113,26 @@ export class HttpUserContext extends EventHub<HttpUserContextEvents> {
|
|
|
88
113
|
return user
|
|
89
114
|
}
|
|
90
115
|
|
|
91
|
-
public async getCurrentUser(request: Pick<IncomingMessage, 'headers'>) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
116
|
+
public async getCurrentUser(request: Pick<IncomingMessage, 'headers'>): Promise<User> {
|
|
117
|
+
const key = request.headers
|
|
118
|
+
const cached = this.userCache.get(key)
|
|
119
|
+
if (cached) {
|
|
120
|
+
return cached
|
|
121
|
+
}
|
|
122
|
+
const pending = this.authenticateRequest(request)
|
|
123
|
+
this.userCache.set(key, pending)
|
|
124
|
+
try {
|
|
125
|
+
return await pending
|
|
126
|
+
} catch (error) {
|
|
127
|
+
this.userCache.delete(key)
|
|
128
|
+
throw error
|
|
95
129
|
}
|
|
96
|
-
return this.user
|
|
97
130
|
}
|
|
98
131
|
|
|
99
132
|
public getSessionIdFromRequest(request: Pick<IncomingMessage, 'headers'>): string | null {
|
|
100
133
|
return extractSessionIdFromCookies(request, this.authentication.cookieName)
|
|
101
134
|
}
|
|
102
135
|
|
|
103
|
-
/**
|
|
104
|
-
* Iterates registered authentication providers in order.
|
|
105
|
-
* - A provider returning `User` means authentication succeeded.
|
|
106
|
-
* - A provider returning `null` means it does not apply; try the next one.
|
|
107
|
-
* - A provider throwing means it owns the request but auth failed; propagate the error.
|
|
108
|
-
*/
|
|
109
136
|
public async authenticateRequest(request: Pick<IncomingMessage, 'headers'>): Promise<User> {
|
|
110
137
|
for (const provider of this.authentication.authenticationProviders) {
|
|
111
138
|
const user = await provider.authenticate(request)
|
|
@@ -114,20 +141,14 @@ export class HttpUserContext extends EventHub<HttpUserContextEvents> {
|
|
|
114
141
|
throw new UnauthenticatedError()
|
|
115
142
|
}
|
|
116
143
|
|
|
117
|
-
/**
|
|
118
|
-
* Creates and sets up a cookie-based session for the provided user
|
|
119
|
-
* @param user The user to create a session for
|
|
120
|
-
* @param serverResponse A serverResponse to set the cookie
|
|
121
|
-
* @returns the current User
|
|
122
|
-
*/
|
|
123
144
|
public async cookieLogin(
|
|
124
145
|
user: User,
|
|
125
|
-
|
|
146
|
+
response: { setHeader: (header: string, value: string) => void },
|
|
126
147
|
): Promise<User> {
|
|
127
148
|
const sessionId = randomBytes(32).toString('hex')
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
this.
|
|
149
|
+
const sessionDataSet = this.systemInjector.get(this.authentication.sessionDataSet)
|
|
150
|
+
await sessionDataSet.add(this.systemInjector, { sessionId, username: user.username })
|
|
151
|
+
response.setHeader('Set-Cookie', `${this.authentication.cookieName}=${sessionId}; Path=/; HttpOnly`)
|
|
131
152
|
this.emit('onLogin', { user })
|
|
132
153
|
return user
|
|
133
154
|
}
|
|
@@ -135,45 +156,47 @@ export class HttpUserContext extends EventHub<HttpUserContextEvents> {
|
|
|
135
156
|
public async cookieLogout(
|
|
136
157
|
request: Pick<IncomingMessage, 'headers'>,
|
|
137
158
|
response: { setHeader: (header: string, value: string) => void },
|
|
138
|
-
) {
|
|
139
|
-
this.
|
|
159
|
+
): Promise<void> {
|
|
160
|
+
this.userCache.delete(request.headers)
|
|
140
161
|
const sessionId = this.getSessionIdFromRequest(request)
|
|
141
162
|
response.setHeader('Set-Cookie', `${this.authentication.cookieName}=; Path=/; HttpOnly`)
|
|
142
163
|
|
|
143
164
|
if (sessionId) {
|
|
144
|
-
const sessionDataSet = this.
|
|
145
|
-
const sessions = await sessionDataSet.find(this.systemInjector, {
|
|
146
|
-
|
|
165
|
+
const sessionDataSet = this.systemInjector.get(this.authentication.sessionDataSet)
|
|
166
|
+
const sessions = await sessionDataSet.find(this.systemInjector, {
|
|
167
|
+
filter: { sessionId: { $eq: sessionId } },
|
|
168
|
+
})
|
|
169
|
+
await sessionDataSet.remove(this.systemInjector, ...sessions.map((s) => s.sessionId))
|
|
147
170
|
}
|
|
148
171
|
this.emit('onLogout', undefined)
|
|
149
172
|
}
|
|
150
173
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
@Injected(PasswordAuthenticator)
|
|
158
|
-
declare private readonly authenticator: PasswordAuthenticator
|
|
159
|
-
|
|
160
|
-
public init() {
|
|
161
|
-
this.getUserDataSet().addListener('onEntityUpdated', ({ id, change }) => {
|
|
162
|
-
if (this.user?.username === id) {
|
|
163
|
-
this.user = { ...this.user, ...change }
|
|
164
|
-
}
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
this.getUserDataSet().addListener('onEntityRemoved', ({ key }) => {
|
|
168
|
-
if (this.user?.username === key) {
|
|
169
|
-
this.user = undefined
|
|
170
|
-
this.emit('onSessionInvalidated', undefined)
|
|
171
|
-
}
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
this.getSessionDataSet().addListener('onEntityRemoved', () => {
|
|
175
|
-
this.user = undefined
|
|
176
|
-
this.emit('onSessionInvalidated', undefined)
|
|
174
|
+
private async getUserByName(userName: string): Promise<User> {
|
|
175
|
+
const userDataSet = this.systemInjector.get(this.authentication.userDataSet)
|
|
176
|
+
const users = await userDataSet.find(this.systemInjector, {
|
|
177
|
+
filter: { username: { $eq: userName } },
|
|
178
|
+
top: 2,
|
|
177
179
|
})
|
|
180
|
+
if (users.length !== 1) {
|
|
181
|
+
throw new UnauthenticatedError()
|
|
182
|
+
}
|
|
183
|
+
return users[0]
|
|
178
184
|
}
|
|
179
185
|
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* DI token for the per-request {@link HttpUserContext}. Scoped — each HTTP
|
|
189
|
+
* request resolves its own instance with a fresh `user` cache.
|
|
190
|
+
*/
|
|
191
|
+
export const HttpUserContext: Token<HttpUserContext, 'scoped'> = defineService({
|
|
192
|
+
name: 'furystack/rest-service/HttpUserContext',
|
|
193
|
+
lifetime: 'scoped',
|
|
194
|
+
factory: ({ inject, injector, onDispose }): HttpUserContext => {
|
|
195
|
+
const authentication = inject(HttpAuthenticationSettings)
|
|
196
|
+
const systemInjector = useSystemIdentityContext({ injector, username: 'HttpUserContext' })
|
|
197
|
+
onDispose(() => systemInjector[Symbol.asyncDispose]())
|
|
198
|
+
// Password authenticator resolution is deferred: tests that exercise only
|
|
199
|
+
// unauthenticated endpoints never have to bind credential stores.
|
|
200
|
+
return new HttpUserContextImpl(authentication, () => injector.get(PasswordAuthenticator), systemInjector)
|
|
201
|
+
},
|
|
202
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export * from './actions/index.js'
|
|
2
2
|
export * from './add-cors-header.js'
|
|
3
|
-
export * from './api-manager.js'
|
|
4
3
|
export * from './authenticate.js'
|
|
5
4
|
export * from './authentication-providers/index.js'
|
|
6
5
|
export * from './authorize.js'
|
|
@@ -8,15 +7,18 @@ export * from './endpoint-generators/index.js'
|
|
|
8
7
|
export * from './get-schema-from-api.js'
|
|
9
8
|
export * from './helpers.js'
|
|
10
9
|
export * from './http-authentication-settings.js'
|
|
10
|
+
export * from './http-server-pool.js'
|
|
11
11
|
export * from './http-user-context.js'
|
|
12
12
|
export * from './login-response-strategy.js'
|
|
13
13
|
export * from './mime-types.js'
|
|
14
14
|
export * from './models/index.js'
|
|
15
|
-
export * from './proxy-
|
|
15
|
+
export * from './proxy-runtime.js'
|
|
16
16
|
export * from './read-post-body.js'
|
|
17
17
|
export * from './request-action-implementation.js'
|
|
18
|
+
export * from './rest-api-runtime.js'
|
|
18
19
|
export * from './schema-validator/index.js'
|
|
19
|
-
export * from './server-manager.js'
|
|
20
20
|
export * from './server-response-extensions.js'
|
|
21
|
-
export * from './
|
|
21
|
+
export * from './server-telemetry.js'
|
|
22
|
+
export * from './static-site-runtime.js'
|
|
23
|
+
export * from './user-store.js'
|
|
22
24
|
export * from './validate.js'
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import type { User } from '@furystack/core'
|
|
2
|
+
import { InMemoryStore, User as UserModel, useSystemIdentityContext } from '@furystack/core'
|
|
3
|
+
import { createInjector, type Injector } from '@furystack/inject'
|
|
4
|
+
import {
|
|
5
|
+
PasswordCredential,
|
|
6
|
+
PasswordCredentialStore,
|
|
7
|
+
PasswordResetToken,
|
|
8
|
+
PasswordResetTokenStore,
|
|
9
|
+
usePasswordPolicy,
|
|
10
|
+
} from '@furystack/security'
|
|
5
11
|
import { usingAsync } from '@furystack/utils'
|
|
6
12
|
import { describe, expect, it } from 'vitest'
|
|
7
|
-
|
|
8
13
|
import { useHttpAuthentication } from './helpers.js'
|
|
9
14
|
import { createCookieLoginStrategy } from './login-response-strategy.js'
|
|
10
15
|
import { DefaultSession } from './models/default-session.js'
|
|
16
|
+
import { SessionDataSet, SessionStore, UserStore } from './user-store.js'
|
|
11
17
|
|
|
12
18
|
const setupInjector = (i: Injector) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const repo = getRepository(i)
|
|
19
|
-
repo.createDataSet(User, 'username')
|
|
20
|
-
repo.createDataSet(DefaultSession, 'sessionId')
|
|
21
|
-
repo.createDataSet(PasswordCredential, 'userName')
|
|
22
|
-
repo.createDataSet(PasswordResetToken, 'token')
|
|
19
|
+
i.bind(UserStore, () => new InMemoryStore({ model: UserModel, primaryKey: 'username' }))
|
|
20
|
+
i.bind(SessionStore, () => new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }))
|
|
21
|
+
i.bind(PasswordCredentialStore, () => new InMemoryStore({ model: PasswordCredential, primaryKey: 'userName' }))
|
|
22
|
+
i.bind(PasswordResetTokenStore, () => new InMemoryStore({ model: PasswordResetToken, primaryKey: 'token' }))
|
|
23
23
|
|
|
24
24
|
usePasswordPolicy(i)
|
|
25
25
|
useHttpAuthentication(i)
|
|
@@ -28,8 +28,8 @@ const setupInjector = (i: Injector) => {
|
|
|
28
28
|
describe('createCookieLoginStrategy', () => {
|
|
29
29
|
const testUser: User = { username: 'testuser', roles: ['admin'] }
|
|
30
30
|
|
|
31
|
-
it('
|
|
32
|
-
await usingAsync(
|
|
31
|
+
it('returns the user as the response body', async () => {
|
|
32
|
+
await usingAsync(createInjector(), async (i) => {
|
|
33
33
|
setupInjector(i)
|
|
34
34
|
const strategy = createCookieLoginStrategy(i)
|
|
35
35
|
const result = await strategy.createLoginResponse(testUser, i)
|
|
@@ -37,8 +37,8 @@ describe('createCookieLoginStrategy', () => {
|
|
|
37
37
|
})
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
it('
|
|
41
|
-
await usingAsync(
|
|
40
|
+
it('returns status code 200', async () => {
|
|
41
|
+
await usingAsync(createInjector(), async (i) => {
|
|
42
42
|
setupInjector(i)
|
|
43
43
|
const strategy = createCookieLoginStrategy(i)
|
|
44
44
|
const result = await strategy.createLoginResponse(testUser, i)
|
|
@@ -46,8 +46,8 @@ describe('createCookieLoginStrategy', () => {
|
|
|
46
46
|
})
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
it('
|
|
50
|
-
await usingAsync(
|
|
49
|
+
it('emits a Set-Cookie header with the generated session id', async () => {
|
|
50
|
+
await usingAsync(createInjector(), async (i) => {
|
|
51
51
|
setupInjector(i)
|
|
52
52
|
const strategy = createCookieLoginStrategy(i)
|
|
53
53
|
const result = await strategy.createLoginResponse(testUser, i)
|
|
@@ -59,23 +59,24 @@ describe('createCookieLoginStrategy', () => {
|
|
|
59
59
|
})
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
-
it('
|
|
63
|
-
await usingAsync(
|
|
62
|
+
it('persists the session via SessionDataSet', async () => {
|
|
63
|
+
await usingAsync(createInjector(), async (i) => {
|
|
64
64
|
setupInjector(i)
|
|
65
65
|
const strategy = createCookieLoginStrategy(i)
|
|
66
66
|
await strategy.createLoginResponse(testUser, i)
|
|
67
67
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
68
|
+
await usingAsync(useSystemIdentityContext({ injector: i, username: 'spec' }), async (systemScope) => {
|
|
69
|
+
const sessionDataSet = systemScope.get(SessionDataSet)
|
|
70
|
+
const sessions = await sessionDataSet.find(systemScope, { filter: { username: { $eq: 'testuser' } } })
|
|
71
|
+
expect(sessions).toHaveLength(1)
|
|
72
|
+
expect(sessions[0].username).toBe('testuser')
|
|
73
|
+
expect(sessions[0].sessionId).toBeTruthy()
|
|
74
|
+
})
|
|
74
75
|
})
|
|
75
76
|
})
|
|
76
77
|
|
|
77
|
-
it('
|
|
78
|
-
await usingAsync(
|
|
78
|
+
it('creates unique session ids for each call', async () => {
|
|
79
|
+
await usingAsync(createInjector(), async (i) => {
|
|
79
80
|
setupInjector(i)
|
|
80
81
|
const strategy = createCookieLoginStrategy(i)
|
|
81
82
|
|
|
@@ -9,14 +9,14 @@ import { JsonResult } from './request-action-implementation.js'
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* A pluggable strategy that turns an authenticated {@link User} into an
|
|
12
|
-
* {@link ActionResult}
|
|
12
|
+
* {@link ActionResult} carrying the session/token payload for the client.
|
|
13
13
|
*
|
|
14
|
-
* Pass a concrete strategy to action factories
|
|
15
|
-
* {@link createPasswordLoginAction} or `createGoogleLoginAction` to
|
|
16
|
-
*
|
|
14
|
+
* Pass a concrete strategy to action factories such as
|
|
15
|
+
* {@link createPasswordLoginAction} or `createGoogleLoginAction` to decouple
|
|
16
|
+
* the authentication mechanism from the session/token mechanism.
|
|
17
17
|
*
|
|
18
|
-
* @typeParam TResult The shape of the response body
|
|
19
|
-
* `{ accessToken
|
|
18
|
+
* @typeParam TResult The shape of the response body — e.g. `User` for
|
|
19
|
+
* cookies, `{ accessToken, refreshToken }` for JWT.
|
|
20
20
|
*/
|
|
21
21
|
export type LoginResponseStrategy<TResult> = {
|
|
22
22
|
createLoginResponse: (user: User, injector: Injector) => Promise<ActionResult<TResult>>
|
|
@@ -25,19 +25,20 @@ export type LoginResponseStrategy<TResult> = {
|
|
|
25
25
|
/**
|
|
26
26
|
* Creates a cookie-based {@link LoginResponseStrategy}.
|
|
27
27
|
*
|
|
28
|
-
* On each login it generates a random session
|
|
29
|
-
* DataSet
|
|
28
|
+
* On each login it generates a random session id, persists it via the
|
|
29
|
+
* configured session DataSet and returns the user with a `Set-Cookie`
|
|
30
|
+
* response header.
|
|
30
31
|
*
|
|
31
|
-
* @param injector The
|
|
32
|
-
*
|
|
32
|
+
* @param injector - The injector scope (typically the root) that carries
|
|
33
|
+
* `HttpAuthenticationSettings` and a persistent session store.
|
|
33
34
|
*/
|
|
34
35
|
export const createCookieLoginStrategy = (injector: Injector): LoginResponseStrategy<User> => {
|
|
35
|
-
const settings = injector.
|
|
36
|
+
const settings = injector.get(HttpAuthenticationSettings)
|
|
36
37
|
const systemInjector = useSystemIdentityContext({ injector, username: 'CookieLoginStrategy' })
|
|
38
|
+
const sessionDataSet = systemInjector.get(settings.sessionDataSet)
|
|
37
39
|
|
|
38
40
|
return {
|
|
39
41
|
createLoginResponse: async (user) => {
|
|
40
|
-
const sessionDataSet = settings.getSessionDataSet(systemInjector)
|
|
41
42
|
const sessionId = randomBytes(32).toString('hex')
|
|
42
43
|
await sessionDataSet.add(systemInjector, { sessionId, username: user.username })
|
|
43
44
|
return JsonResult(user, 200, {
|