@furystack/rest-service 13.0.0 → 14.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 +124 -0
- package/README.md +47 -28
- package/esm/actions/error-action.d.ts +7 -2
- package/esm/actions/error-action.d.ts.map +1 -1
- package/esm/actions/error-action.js +7 -2
- package/esm/actions/error-action.js.map +1 -1
- package/esm/actions/get-current-user.d.ts +1 -4
- package/esm/actions/get-current-user.d.ts.map +1 -1
- package/esm/actions/get-current-user.js +1 -4
- package/esm/actions/get-current-user.js.map +1 -1
- package/esm/actions/get-current-user.spec.js +10 -6
- package/esm/actions/get-current-user.spec.js.map +1 -1
- package/esm/actions/is-authenticated.d.ts +3 -4
- package/esm/actions/is-authenticated.d.ts.map +1 -1
- package/esm/actions/is-authenticated.js +3 -4
- package/esm/actions/is-authenticated.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.d.ts +2 -6
- package/esm/actions/logout.d.ts.map +1 -1
- package/esm/actions/logout.js +3 -7
- package/esm/actions/logout.js.map +1 -1
- package/esm/actions/not-found-action.d.ts +1 -3
- package/esm/actions/not-found-action.d.ts.map +1 -1
- package/esm/actions/not-found-action.js +1 -3
- package/esm/actions/not-found-action.js.map +1 -1
- package/esm/actions/password-login-action.d.ts +5 -12
- package/esm/actions/password-login-action.d.ts.map +1 -1
- package/esm/actions/password-login-action.js +6 -13
- 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.d.ts +5 -11
- package/esm/authenticate.d.ts.map +1 -1
- package/esm/authenticate.js +6 -12
- package/esm/authenticate.js.map +1 -1
- package/esm/authenticate.spec.js +41 -33
- package/esm/authenticate.spec.js.map +1 -1
- package/esm/authentication-providers/authentication-provider.d.ts +17 -0
- package/esm/authentication-providers/authentication-provider.d.ts.map +1 -1
- package/esm/authentication-providers/basic-auth-provider.d.ts +7 -0
- package/esm/authentication-providers/basic-auth-provider.d.ts.map +1 -1
- package/esm/authentication-providers/basic-auth-provider.js +7 -0
- package/esm/authentication-providers/basic-auth-provider.js.map +1 -1
- package/esm/authentication-providers/cookie-auth-provider.d.ts.map +1 -1
- package/esm/authentication-providers/cookie-auth-provider.js +8 -0
- package/esm/authentication-providers/cookie-auth-provider.js.map +1 -1
- package/esm/authorize.d.ts +5 -17
- package/esm/authorize.d.ts.map +1 -1
- package/esm/authorize.js +5 -17
- package/esm/authorize.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 -1
- 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 -1
- 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/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.js +1 -0
- package/esm/get-schema-from-api.js.map +1 -1
- package/esm/get-schema-from-api.spec.js.map +1 -1
- package/esm/helpers.d.ts +54 -88
- package/esm/helpers.d.ts.map +1 -1
- package/esm/helpers.js +90 -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 +56 -12
- package/esm/http-authentication-settings.d.ts.map +1 -1
- package/esm/http-authentication-settings.js +27 -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 +34 -44
- package/esm/http-user-context.d.ts.map +1 -1
- package/esm/http-user-context.js +75 -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 +226 -332
- package/esm/http-user-context.spec.js.map +1 -1
- package/esm/identity-cache-keys.d.ts +28 -0
- package/esm/identity-cache-keys.d.ts.map +1 -0
- package/esm/identity-cache-keys.js +13 -0
- package/esm/identity-cache-keys.js.map +1 -0
- package/esm/identity-cache-keys.spec.d.ts +2 -0
- package/esm/identity-cache-keys.spec.d.ts.map +1 -0
- package/esm/identity-cache-keys.spec.js +21 -0
- package/esm/identity-cache-keys.spec.js.map +1 -0
- package/esm/identity-event-bus.d.ts +102 -0
- package/esm/identity-event-bus.d.ts.map +1 -0
- package/esm/identity-event-bus.js +151 -0
- package/esm/identity-event-bus.js.map +1 -0
- package/esm/identity-event-bus.spec.d.ts +2 -0
- package/esm/identity-event-bus.spec.d.ts.map +1 -0
- package/esm/identity-event-bus.spec.js +392 -0
- package/esm/identity-event-bus.spec.js.map +1 -0
- package/esm/index.d.ts +9 -4
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +9 -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.spec.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/request-action-implementation.d.ts +25 -0
- package/esm/request-action-implementation.d.ts.map +1 -1
- package/esm/request-action-implementation.js +9 -0
- package/esm/request-action-implementation.js.map +1 -1
- 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 +57 -80
- 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 +49 -0
- package/esm/server-telemetry.d.ts.map +1 -0
- package/esm/server-telemetry.js +25 -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-resolution-cache.d.ts +65 -0
- package/esm/user-resolution-cache.d.ts.map +1 -0
- package/esm/user-resolution-cache.js +101 -0
- package/esm/user-resolution-cache.js.map +1 -0
- package/esm/user-resolution-cache.spec.d.ts +2 -0
- package/esm/user-resolution-cache.spec.d.ts.map +1 -0
- package/esm/user-resolution-cache.spec.js +220 -0
- package/esm/user-resolution-cache.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.d.ts +13 -12
- package/esm/validate.d.ts.map +1 -1
- package/esm/validate.integration.spec.js +27 -28
- package/esm/validate.integration.spec.js.map +1 -1
- package/esm/validate.js +12 -0
- package/esm/validate.js.map +1 -1
- package/package.json +14 -12
- package/src/actions/error-action.ts +7 -3
- package/src/actions/get-current-user.spec.ts +11 -10
- package/src/actions/get-current-user.ts +1 -4
- package/src/actions/is-authenticated.spec.ts +12 -7
- package/src/actions/is-authenticated.ts +3 -4
- package/src/actions/logout-action.spec.ts +12 -12
- package/src/actions/logout.ts +3 -7
- package/src/actions/not-found-action.ts +1 -3
- package/src/actions/password-login-action.spec.ts +30 -30
- package/src/actions/password-login-action.ts +6 -13
- package/src/authenticate.spec.ts +45 -51
- package/src/authenticate.ts +6 -12
- package/src/authentication-providers/authentication-provider.ts +17 -0
- package/src/authentication-providers/basic-auth-provider.ts +7 -0
- package/src/authentication-providers/cookie-auth-provider.ts +8 -0
- package/src/authorize.spec.ts +37 -39
- package/src/authorize.ts +6 -17
- 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 -2
- 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/get-schema-from-api.spec.ts +1 -1
- package/src/get-schema-from-api.ts +2 -1
- package/src/helpers.spec.ts +51 -41
- package/src/helpers.ts +140 -109
- package/src/http-authentication-settings.ts +78 -24
- package/src/http-server-pool.spec.ts +220 -0
- package/src/http-server-pool.ts +169 -0
- package/src/http-user-context.spec.ts +246 -378
- package/src/http-user-context.ts +122 -92
- package/src/identity-cache-keys.spec.ts +24 -0
- package/src/identity-cache-keys.ts +31 -0
- package/src/identity-event-bus.spec.ts +285 -0
- package/src/identity-event-bus.ts +230 -0
- package/src/index.ts +9 -4
- package/src/login-response-strategy.spec.ts +32 -31
- package/src/login-response-strategy.ts +14 -12
- package/src/openapi/generate-openapi-document.spec.ts +2 -2
- package/src/proxy-manager.spec.ts +158 -188
- package/src/proxy-runtime.spec.ts +50 -0
- package/src/proxy-runtime.ts +75 -0
- package/src/request-action-implementation.ts +25 -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 +65 -85
- package/src/rest.integration.test.ts +27 -87
- package/src/server-telemetry.spec.ts +56 -0
- package/src/server-telemetry.ts +43 -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-resolution-cache.spec.ts +241 -0
- package/src/user-resolution-cache.ts +164 -0
- package/src/user-store.spec.ts +86 -0
- package/src/user-store.ts +69 -0
- package/src/validate.integration.spec.ts +40 -35
- package/src/validate.ts +14 -12
- package/esm/api-manager.d.ts +0 -104
- package/esm/api-manager.d.ts.map +0 -1
- package/esm/api-manager.js +0 -179
- 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/src/api-manager.ts +0 -304
- package/src/proxy-manager.ts +0 -102
- package/src/server-manager.ts +0 -135
- package/src/static-server-manager.ts +0 -90
|
@@ -4,10 +4,7 @@ import { Authenticate } from '../authenticate.js'
|
|
|
4
4
|
import type { RequestAction } from '../request-action-implementation.js'
|
|
5
5
|
import { JsonResult } from '../request-action-implementation.js'
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Action that returns the current authenticated user
|
|
9
|
-
* @param injector The injector from the current stack
|
|
10
|
-
*/
|
|
7
|
+
/** Returns the current authenticated user; 401 if no user is bound. */
|
|
11
8
|
export const GetCurrentUser: RequestAction<{
|
|
12
9
|
result: User
|
|
13
10
|
}> = Authenticate()(async ({ injector }) => {
|
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IdentityContext } from '@furystack/core'
|
|
2
|
+
import { createInjector } from '@furystack/inject'
|
|
2
3
|
import { usingAsync } from '@furystack/utils'
|
|
3
|
-
import { IsAuthenticated } from './is-authenticated.js'
|
|
4
4
|
import type { IncomingMessage, ServerResponse } from 'http'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
import { IsAuthenticated } from './is-authenticated.js'
|
|
7
7
|
|
|
8
8
|
describe('isAuthenticated', () => {
|
|
9
9
|
const request = { url: 'https://google.com' } as IncomingMessage
|
|
10
10
|
const response = {} as ServerResponse
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
|
|
12
|
+
it('returns the authentication status from the identity context', async () => {
|
|
13
|
+
await usingAsync(createInjector(), async (i) => {
|
|
14
|
+
i.bind(IdentityContext, () => ({
|
|
15
|
+
isAuthenticated: () => Promise.resolve(true),
|
|
16
|
+
isAuthorized: () => Promise.resolve(true),
|
|
17
|
+
getCurrentUser: () => Promise.reject(new Error('not needed')),
|
|
18
|
+
}))
|
|
14
19
|
const result = await IsAuthenticated({ injector: i, request, response })
|
|
15
20
|
expect(result.statusCode).toBe(200)
|
|
16
21
|
expect(result.chunk).toEqual({ isAuthenticated: true })
|
|
@@ -3,10 +3,9 @@ import type { RequestAction } from '../request-action-implementation.js'
|
|
|
3
3
|
import { JsonResult } from '../request-action-implementation.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* @returns A standard authentication result
|
|
6
|
+
* Returns `{ isAuthenticated: boolean }`. Unauthenticated callers receive
|
|
7
|
+
* a 200 with `false` rather than a 401 — use this when the client needs to
|
|
8
|
+
* branch on auth state without triggering a re-login flow.
|
|
10
9
|
*/
|
|
11
10
|
export const IsAuthenticated: RequestAction<{ result: { isAuthenticated: boolean } }> = async ({ injector }) => {
|
|
12
11
|
const isAuthenticatedResult = await isAuthenticated(injector)
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createInjector } from '@furystack/inject'
|
|
2
|
+
import { EventHub } from '@furystack/utils'
|
|
2
3
|
import { usingAsync } from '@furystack/utils'
|
|
3
4
|
import type { IncomingMessage, ServerResponse } from 'http'
|
|
4
5
|
import { describe, expect, it, vi } from 'vitest'
|
|
5
|
-
import { HttpUserContext } from '../http-user-context.js'
|
|
6
|
+
import type { HttpUserContext } from '../http-user-context.js'
|
|
7
|
+
import { HttpUserContext as HttpUserContextToken } from '../http-user-context.js'
|
|
6
8
|
import { LogoutAction } from './logout.js'
|
|
7
9
|
|
|
8
10
|
describe('LogoutAction', () => {
|
|
9
11
|
const request = { url: 'https://google.com' } as IncomingMessage
|
|
10
12
|
const response = {} as ServerResponse
|
|
11
13
|
|
|
12
|
-
it('
|
|
13
|
-
await usingAsync(
|
|
14
|
-
const cookieLogout = vi.fn(async () =>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
HttpUserContext,
|
|
20
|
-
)
|
|
14
|
+
it('delegates to HttpUserContext.cookieLogout and returns an empty result', async () => {
|
|
15
|
+
await usingAsync(createInjector(), async (i) => {
|
|
16
|
+
const cookieLogout = vi.fn(async () => {
|
|
17
|
+
/* noop */
|
|
18
|
+
})
|
|
19
|
+
const stub = Object.assign(new EventHub(), { cookieLogout }) as unknown as HttpUserContext
|
|
20
|
+
i.bind(HttpUserContextToken, () => stub)
|
|
21
21
|
|
|
22
22
|
const result = await LogoutAction({ request, response, injector: i })
|
|
23
23
|
expect(result.statusCode).toBe(200)
|
|
24
24
|
expect(result.chunk).toBe(undefined)
|
|
25
|
-
expect(cookieLogout).
|
|
25
|
+
expect(cookieLogout).toHaveBeenCalled()
|
|
26
26
|
})
|
|
27
27
|
})
|
|
28
28
|
})
|
package/src/actions/logout.ts
CHANGED
|
@@ -3,14 +3,10 @@ import type { RequestAction } from '../request-action-implementation.js'
|
|
|
3
3
|
import { EmptyResult } from '../request-action-implementation.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* @param root0.injector The injector from the context
|
|
9
|
-
* @param root0.request The current Request object
|
|
10
|
-
* @param root0.response The Response object
|
|
11
|
-
* @returns An empty result that indicates the success
|
|
6
|
+
* Logs out via {@link HttpUserContext.cookieLogout} — clears the session
|
|
7
|
+
* cookie and removes the session row from the session DataSet.
|
|
12
8
|
*/
|
|
13
9
|
export const LogoutAction: RequestAction<{ result: unknown }> = async ({ injector, request, response }) => {
|
|
14
|
-
await injector.
|
|
10
|
+
await injector.get(HttpUserContext).cookieLogout(request, response)
|
|
15
11
|
return EmptyResult()
|
|
16
12
|
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import type { RequestAction } from '../request-action-implementation.js'
|
|
2
2
|
import { JsonResult } from '../request-action-implementation.js'
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* @returns The standard Not Found action result
|
|
6
|
-
*/
|
|
4
|
+
/** Default `404` action. Returns `{ error: 'Content not found' }`. */
|
|
7
5
|
export const NotFoundAction: RequestAction<{ result: { error: string } }> = async () => {
|
|
8
6
|
return JsonResult(
|
|
9
7
|
{
|
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import type { User } from '@furystack/core'
|
|
2
|
+
import { InMemoryStore, User as UserModel } 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 type { IncomingMessage, ServerResponse } from 'http'
|
|
7
13
|
import { describe, expect, it, vi } from 'vitest'
|
|
8
|
-
|
|
9
14
|
import { useHttpAuthentication } from '../helpers.js'
|
|
10
15
|
import type { LoginResponseStrategy } from '../login-response-strategy.js'
|
|
11
16
|
import { DefaultSession } from '../models/default-session.js'
|
|
12
17
|
import { JsonResult } from '../request-action-implementation.js'
|
|
18
|
+
import { SessionStore, UserStore } from '../user-store.js'
|
|
19
|
+
import { PasswordAuthenticator } from '@furystack/security'
|
|
13
20
|
import { createPasswordLoginAction } from './password-login-action.js'
|
|
14
21
|
|
|
15
22
|
const setupInjector = async (i: Injector, username: string, password: string) => {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const repo = getRepository(i)
|
|
22
|
-
repo.createDataSet(User, 'username')
|
|
23
|
-
repo.createDataSet(DefaultSession, 'sessionId')
|
|
24
|
-
repo.createDataSet(PasswordCredential, 'userName')
|
|
25
|
-
repo.createDataSet(PasswordResetToken, 'token')
|
|
23
|
+
i.bind(UserStore, () => new InMemoryStore({ model: UserModel, primaryKey: 'username' }))
|
|
24
|
+
i.bind(SessionStore, () => new InMemoryStore({ model: DefaultSession, primaryKey: 'sessionId' }))
|
|
25
|
+
i.bind(PasswordCredentialStore, () => new InMemoryStore({ model: PasswordCredential, primaryKey: 'userName' }))
|
|
26
|
+
i.bind(PasswordResetTokenStore, () => new InMemoryStore({ model: PasswordResetToken, primaryKey: 'token' }))
|
|
26
27
|
|
|
27
28
|
usePasswordPolicy(i)
|
|
28
29
|
useHttpAuthentication(i)
|
|
29
30
|
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
await
|
|
34
|
-
await sm.getStoreFor(User, 'username').add({ username, roles: ['admin'] })
|
|
31
|
+
const authenticator = i.get(PasswordAuthenticator)
|
|
32
|
+
const cred = await authenticator.hasher.createCredential(username, password)
|
|
33
|
+
await i.get(PasswordCredentialStore).add(cred)
|
|
34
|
+
await i.get(UserStore).add({ username, roles: ['admin'] })
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const mockStrategy: LoginResponseStrategy<User> = {
|
|
@@ -42,8 +42,8 @@ describe('createPasswordLoginAction', () => {
|
|
|
42
42
|
const request = {} as IncomingMessage
|
|
43
43
|
const response = {} as ServerResponse
|
|
44
44
|
|
|
45
|
-
it('
|
|
46
|
-
await usingAsync(
|
|
45
|
+
it('delegates to the strategy on successful authentication', async () => {
|
|
46
|
+
await usingAsync(createInjector(), async (i) => {
|
|
47
47
|
await setupInjector(i, 'testuser', 'testpass')
|
|
48
48
|
const action = createPasswordLoginAction(mockStrategy)
|
|
49
49
|
const result = await action({
|
|
@@ -57,8 +57,8 @@ describe('createPasswordLoginAction', () => {
|
|
|
57
57
|
})
|
|
58
58
|
})
|
|
59
59
|
|
|
60
|
-
it('
|
|
61
|
-
await usingAsync(
|
|
60
|
+
it('passes the authenticated user to the strategy', async () => {
|
|
61
|
+
await usingAsync(createInjector(), async (i) => {
|
|
62
62
|
await setupInjector(i, 'testuser', 'testpass')
|
|
63
63
|
const strategyFn = vi.fn(async (user: User) => JsonResult(user, 200))
|
|
64
64
|
const strategy: LoginResponseStrategy<User> = { createLoginResponse: strategyFn }
|
|
@@ -73,8 +73,8 @@ describe('createPasswordLoginAction', () => {
|
|
|
73
73
|
})
|
|
74
74
|
})
|
|
75
75
|
|
|
76
|
-
it('
|
|
77
|
-
await usingAsync(
|
|
76
|
+
it('throws RequestError on invalid password', async () => {
|
|
77
|
+
await usingAsync(createInjector(), async (i) => {
|
|
78
78
|
await setupInjector(i, 'testuser', 'testpass')
|
|
79
79
|
const action = createPasswordLoginAction(mockStrategy)
|
|
80
80
|
await expect(
|
|
@@ -88,8 +88,8 @@ describe('createPasswordLoginAction', () => {
|
|
|
88
88
|
})
|
|
89
89
|
})
|
|
90
90
|
|
|
91
|
-
it('
|
|
92
|
-
await usingAsync(
|
|
91
|
+
it('throws RequestError for an unknown user', async () => {
|
|
92
|
+
await usingAsync(createInjector(), async (i) => {
|
|
93
93
|
await setupInjector(i, 'testuser', 'testpass')
|
|
94
94
|
const action = createPasswordLoginAction(mockStrategy)
|
|
95
95
|
await expect(
|
|
@@ -103,8 +103,8 @@ describe('createPasswordLoginAction', () => {
|
|
|
103
103
|
})
|
|
104
104
|
})
|
|
105
105
|
|
|
106
|
-
it('
|
|
107
|
-
await usingAsync(
|
|
106
|
+
it('supports strategies returning custom result shapes', async () => {
|
|
107
|
+
await usingAsync(createInjector(), async (i) => {
|
|
108
108
|
await setupInjector(i, 'testuser', 'testpass')
|
|
109
109
|
const tokenStrategy: LoginResponseStrategy<{ accessToken: string }> = {
|
|
110
110
|
createLoginResponse: async () => JsonResult({ accessToken: 'tok123' }, 200),
|
|
@@ -6,18 +6,11 @@ import type { LoginResponseStrategy } from '../login-response-strategy.js'
|
|
|
6
6
|
import type { RequestAction } from '../request-action-implementation.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* - Cookie strategy -> `ActionResult<User>`
|
|
15
|
-
* - JWT strategy -> `ActionResult<{ accessToken: string; refreshToken: string }>`
|
|
16
|
-
*
|
|
17
|
-
* A random delay (0-1 s) is added on failure to mitigate timing attacks.
|
|
18
|
-
*
|
|
19
|
-
* @param strategy The login response strategy that produces the session/token result
|
|
20
|
-
* @returns A `RequestAction` that can be wired into a REST API route
|
|
9
|
+
* Username + password login action. Authenticates via {@link HttpUserContext},
|
|
10
|
+
* then delegates session/token creation to `strategy`. The result type is
|
|
11
|
+
* inferred from the strategy (e.g. `User` for cookies, `{ accessToken,
|
|
12
|
+
* refreshToken }` for JWT). Adds a 0–1 s random delay on failure to mitigate
|
|
13
|
+
* timing attacks; rethrows as `RequestError(400)`.
|
|
21
14
|
*/
|
|
22
15
|
export const createPasswordLoginAction = <TResult>(
|
|
23
16
|
strategy: LoginResponseStrategy<TResult>,
|
|
@@ -25,7 +18,7 @@ export const createPasswordLoginAction = <TResult>(
|
|
|
25
18
|
return async ({ injector, getBody }) => {
|
|
26
19
|
const body = await getBody()
|
|
27
20
|
try {
|
|
28
|
-
const user = await injector.
|
|
21
|
+
const user = await injector.get(HttpUserContext).authenticateUser(body.username, body.password)
|
|
29
22
|
return strategy.createLoginResponse(user, injector)
|
|
30
23
|
} catch {
|
|
31
24
|
await sleepAsync(Math.random() * 1000)
|
package/src/authenticate.spec.ts
CHANGED
|
@@ -1,84 +1,78 @@
|
|
|
1
1
|
import { IdentityContext } from '@furystack/core'
|
|
2
|
-
import {
|
|
3
|
-
import { usingAsync } from '@furystack/utils'
|
|
2
|
+
import { createInjector } from '@furystack/inject'
|
|
3
|
+
import { EventHub, usingAsync } from '@furystack/utils'
|
|
4
4
|
import type { IncomingMessage, ServerResponse } from 'http'
|
|
5
5
|
import { describe, expect, it, vi } from 'vitest'
|
|
6
6
|
import { Authenticate } from './authenticate.js'
|
|
7
|
-
import { HttpUserContext } from './http-user-context.js'
|
|
7
|
+
import type { HttpUserContext } from './http-user-context.js'
|
|
8
|
+
import { HttpUserContext as HttpUserContextToken } from './http-user-context.js'
|
|
8
9
|
import { EmptyResult } from './request-action-implementation.js'
|
|
9
10
|
|
|
11
|
+
const makeHttpUserContextStub = (providers: Array<{ name: string }>): HttpUserContext =>
|
|
12
|
+
Object.assign(new EventHub(), {
|
|
13
|
+
authentication: { authenticationProviders: providers },
|
|
14
|
+
}) as unknown as HttpUserContext
|
|
15
|
+
|
|
10
16
|
describe('Authenticate', () => {
|
|
11
|
-
const response = {} as
|
|
17
|
+
const response = {} as ServerResponse
|
|
12
18
|
const request = { url: 'http://google.com' } as IncomingMessage
|
|
13
19
|
|
|
14
|
-
it('
|
|
15
|
-
await usingAsync(
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
)
|
|
20
|
+
it('returns 401 without WWW-Authenticate when no basic-auth provider is registered', async () => {
|
|
21
|
+
await usingAsync(createInjector(), async (i) => {
|
|
22
|
+
const isAuthenticated = vi.fn(async () => false)
|
|
23
|
+
i.bind(IdentityContext, () => ({
|
|
24
|
+
isAuthenticated,
|
|
25
|
+
getCurrentUser: () => Promise.reject(new Error(':(')),
|
|
26
|
+
isAuthorized: () => Promise.resolve(false),
|
|
27
|
+
}))
|
|
28
|
+
i.bind(HttpUserContextToken, () => makeHttpUserContextStub([]))
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
authentication: { authenticationProviders: [] },
|
|
26
|
-
},
|
|
27
|
-
HttpUserContext,
|
|
28
|
-
)
|
|
29
|
-
const exampleAuthenticatedAction = vi.fn(async () => EmptyResult())
|
|
30
|
-
const authorized = Authenticate()(exampleAuthenticatedAction)
|
|
30
|
+
const exampleAction = vi.fn(async () => EmptyResult())
|
|
31
|
+
const authorized = Authenticate()(exampleAction)
|
|
31
32
|
|
|
32
33
|
const result = await authorized({ injector: i, request, response })
|
|
33
34
|
expect(result.statusCode).toBe(401)
|
|
34
35
|
expect(result.chunk).toEqual({ error: 'unauthorized' })
|
|
35
36
|
expect(result.headers).toEqual({ 'Content-Type': 'application/json' })
|
|
36
|
-
expect(
|
|
37
|
+
expect(exampleAction).not.toHaveBeenCalled()
|
|
37
38
|
})
|
|
38
39
|
})
|
|
39
40
|
|
|
40
|
-
it('
|
|
41
|
-
await usingAsync(
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
)
|
|
41
|
+
it('returns 401 with WWW-Authenticate: Basic when a basic-auth provider is registered', async () => {
|
|
42
|
+
await usingAsync(createInjector(), async (i) => {
|
|
43
|
+
const isAuthenticated = vi.fn(async () => false)
|
|
44
|
+
i.bind(IdentityContext, () => ({
|
|
45
|
+
isAuthenticated,
|
|
46
|
+
getCurrentUser: () => Promise.reject(new Error(':(')),
|
|
47
|
+
isAuthorized: () => Promise.resolve(false),
|
|
48
|
+
}))
|
|
49
|
+
i.bind(HttpUserContextToken, () => makeHttpUserContextStub([{ name: 'basic-auth' }]))
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
authentication: {
|
|
52
|
-
authenticationProviders: [{ name: 'basic-auth', authenticate: async () => null }],
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
HttpUserContext,
|
|
56
|
-
)
|
|
57
|
-
const exampleAuthenticatedAction = vi.fn(async () => EmptyResult())
|
|
58
|
-
const authorized = Authenticate()(exampleAuthenticatedAction)
|
|
51
|
+
const exampleAction = vi.fn(async () => EmptyResult())
|
|
52
|
+
const authorized = Authenticate()(exampleAction)
|
|
59
53
|
|
|
60
54
|
const result = await authorized({ injector: i, request, response })
|
|
61
55
|
expect(result.statusCode).toBe(401)
|
|
62
56
|
expect(result.chunk).toEqual({ error: 'unauthorized' })
|
|
63
57
|
expect(result.headers).toEqual({ 'Content-Type': 'application/json', 'WWW-Authenticate': 'Basic' })
|
|
64
|
-
expect(
|
|
58
|
+
expect(exampleAction).not.toHaveBeenCalled()
|
|
65
59
|
})
|
|
66
60
|
})
|
|
67
61
|
|
|
68
|
-
it('
|
|
69
|
-
await usingAsync(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
75
|
-
const
|
|
76
|
-
const authorized = Authenticate()(
|
|
77
|
-
const params = { injector: i,
|
|
62
|
+
it('invokes the wrapped action when the caller is authenticated', async () => {
|
|
63
|
+
await usingAsync(createInjector(), async (i) => {
|
|
64
|
+
i.bind(IdentityContext, () => ({
|
|
65
|
+
isAuthenticated: () => Promise.resolve(true),
|
|
66
|
+
getCurrentUser: () => Promise.reject(new Error(':(')),
|
|
67
|
+
isAuthorized: () => Promise.resolve(true),
|
|
68
|
+
}))
|
|
69
|
+
const exampleAction = vi.fn(async () => EmptyResult())
|
|
70
|
+
const authorized = Authenticate()(exampleAction)
|
|
71
|
+
const params = { injector: i, request, response }
|
|
78
72
|
const result = await authorized(params)
|
|
79
73
|
expect(result.statusCode).toBe(200)
|
|
80
74
|
expect(result.chunk).toBe(undefined)
|
|
81
|
-
expect(
|
|
75
|
+
expect(exampleAction).toHaveBeenCalledWith(params)
|
|
82
76
|
})
|
|
83
77
|
})
|
|
84
78
|
})
|
package/src/authenticate.ts
CHANGED
|
@@ -5,21 +5,15 @@ import type { ActionResult, RequestAction, RequestActionOptions } from './reques
|
|
|
5
5
|
import { JsonResult } from './request-action-implementation.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* Wraps a {@link RequestAction} to require authentication. Unauthenticated
|
|
9
|
+
* requests return `401 Unauthorized` (with `WWW-Authenticate: Basic` when
|
|
10
|
+
* the chain includes Basic Auth). Adds a 0–1 s random delay on rejection to
|
|
11
|
+
* mitigate timing attacks.
|
|
11
12
|
*
|
|
12
|
-
* @returns A function that wraps the provided action with authentication check
|
|
13
13
|
* @example
|
|
14
14
|
* ```ts
|
|
15
|
-
* // Wrap an endpoint to require authentication
|
|
16
|
-
* const protectedEndpoint = Authenticate()(myEndpoint)
|
|
17
|
-
*
|
|
18
|
-
* // Or use directly in API definition
|
|
19
15
|
* api: {
|
|
20
|
-
* GET: {
|
|
21
|
-
* '/users': Authenticate()(getUsersEndpoint)
|
|
22
|
-
* }
|
|
16
|
+
* GET: { '/users': Authenticate()(getUsersEndpoint) },
|
|
23
17
|
* }
|
|
24
18
|
* ```
|
|
25
19
|
*/
|
|
@@ -32,7 +26,7 @@ export const Authenticate =
|
|
|
32
26
|
if (!authenticated) {
|
|
33
27
|
await sleepAsync(Math.random() * 1000)
|
|
34
28
|
const hasBasicAuth = injector
|
|
35
|
-
.
|
|
29
|
+
.get(HttpUserContext)
|
|
36
30
|
.authentication.authenticationProviders.some((p) => p.name === 'basic-auth')
|
|
37
31
|
return JsonResult(
|
|
38
32
|
{ error: 'unauthorized' },
|
|
@@ -22,4 +22,21 @@ export type AuthenticationProvider = {
|
|
|
22
22
|
*/
|
|
23
23
|
readonly name: string
|
|
24
24
|
authenticate: (request: Pick<IncomingMessage, 'headers'>) => Promise<User | null>
|
|
25
|
+
/**
|
|
26
|
+
* Optional cache-key resolver consulted by the user-resolution cache before
|
|
27
|
+
* the provider chain is walked.
|
|
28
|
+
*
|
|
29
|
+
* Returns a stable, opaque string identifying the authenticated principal
|
|
30
|
+
* for the given request, or `null` when this provider does not apply to
|
|
31
|
+
* the request (or refuses to participate in caching — e.g. Basic Auth,
|
|
32
|
+
* which must verify the password on every request).
|
|
33
|
+
*
|
|
34
|
+
* The first non-null key returned by the configured providers is used as
|
|
35
|
+
* the cache key. Keys returned by different providers must not collide;
|
|
36
|
+
* built-in providers prefix their keys (`cookie:`, `jwt:`).
|
|
37
|
+
*
|
|
38
|
+
* If no provider returns a key, the request bypasses the cache entirely
|
|
39
|
+
* and the provider chain is walked on every call.
|
|
40
|
+
*/
|
|
41
|
+
readonly getCacheKey?: (request: Pick<IncomingMessage, 'headers'>) => string | null
|
|
25
42
|
}
|
|
@@ -3,6 +3,13 @@ import type { AuthenticationProvider } from './authentication-provider.js'
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Creates an authentication provider for HTTP Basic Authentication.
|
|
6
|
+
*
|
|
7
|
+
* **Caching note:** this provider intentionally does **not** implement
|
|
8
|
+
* `getCacheKey`. Caching authenticated users by username would bypass
|
|
9
|
+
* password verification on every cache hit — the cache cannot revalidate the
|
|
10
|
+
* password without re-running the authenticator. Basic Auth requests
|
|
11
|
+
* therefore always walk the provider chain, which is also where the password
|
|
12
|
+
* is verified.
|
|
6
13
|
* @param authenticateUser Callback that verifies credentials and returns a User or throws
|
|
7
14
|
*/
|
|
8
15
|
export const createBasicAuthProvider = (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { User } from '@furystack/core'
|
|
2
2
|
import { UnauthenticatedError } from '@furystack/security'
|
|
3
|
+
import { sessionCacheKey, type SessionCacheKey } from '../identity-cache-keys.js'
|
|
3
4
|
import type { DefaultSession } from '../models/default-session.js'
|
|
4
5
|
import type { AuthenticationProvider } from './authentication-provider.js'
|
|
5
6
|
import { extractSessionIdFromCookies } from './helpers.js'
|
|
@@ -23,4 +24,11 @@ export const createCookieAuthProvider = (
|
|
|
23
24
|
if (!session) throw new UnauthenticatedError()
|
|
24
25
|
return await getUserByName(session.username)
|
|
25
26
|
},
|
|
27
|
+
// The session id alone uniquely identifies the principal — caching by it is
|
|
28
|
+
// safe because session invalidation (logout, expiry) drops the session row,
|
|
29
|
+
// and the TTL on the user-resolution cache bounds staleness across nodes.
|
|
30
|
+
getCacheKey: (request): SessionCacheKey | null => {
|
|
31
|
+
const sessionId = extractSessionIdFromCookies(request, cookieName)
|
|
32
|
+
return sessionId ? sessionCacheKey(sessionId) : null
|
|
33
|
+
},
|
|
26
34
|
})
|
package/src/authorize.spec.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { User } from '@furystack/core'
|
|
2
2
|
import { AuthorizationError, IdentityContext } from '@furystack/core'
|
|
3
|
-
import {
|
|
3
|
+
import { createInjector } from '@furystack/inject'
|
|
4
4
|
import { usingAsync } from '@furystack/utils'
|
|
5
5
|
import type { IncomingMessage, ServerResponse } from 'http'
|
|
6
6
|
import { describe, expect, it, vi } from 'vitest'
|
|
@@ -8,59 +8,57 @@ import { Authorize } from './authorize.js'
|
|
|
8
8
|
import { EmptyResult } from './request-action-implementation.js'
|
|
9
9
|
|
|
10
10
|
describe('Authorize', () => {
|
|
11
|
-
const response = {} as
|
|
11
|
+
const response = {} as ServerResponse
|
|
12
12
|
const request = { url: 'http://google.com' } as IncomingMessage
|
|
13
13
|
|
|
14
|
-
it('
|
|
15
|
-
await usingAsync(
|
|
16
|
-
const
|
|
17
|
-
i.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
14
|
+
it('throws AuthorizationError when the current user cannot be resolved', async () => {
|
|
15
|
+
await usingAsync(createInjector(), async (i) => {
|
|
16
|
+
const isAuthorized = vi.fn(async () => false)
|
|
17
|
+
i.bind(IdentityContext, () => ({
|
|
18
|
+
isAuthorized,
|
|
19
|
+
getCurrentUser: () => Promise.reject(new Error(':(')),
|
|
20
|
+
isAuthenticated: () => Promise.resolve(false),
|
|
21
|
+
}))
|
|
22
|
+
const exampleAction = vi.fn(async () => EmptyResult())
|
|
23
|
+
const authorized = Authorize('Role1')(exampleAction)
|
|
23
24
|
|
|
24
25
|
await expect(() => authorized({ injector: i, request, response })).rejects.toThrow(AuthorizationError)
|
|
25
|
-
expect(
|
|
26
|
+
expect(exampleAction).not.toHaveBeenCalled()
|
|
26
27
|
})
|
|
27
28
|
})
|
|
28
29
|
|
|
29
|
-
it('
|
|
30
|
-
await usingAsync(
|
|
31
|
-
const
|
|
32
|
-
i.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
)
|
|
39
|
-
const
|
|
40
|
-
const authorized = Authorize('Role2')(exampleAuthorizedAction)
|
|
30
|
+
it('throws AuthorizationError when the current user does not have the required role', async () => {
|
|
31
|
+
await usingAsync(createInjector(), async (i) => {
|
|
32
|
+
const isAuthorized = vi.fn(async () => false)
|
|
33
|
+
i.bind(IdentityContext, () => ({
|
|
34
|
+
isAuthorized,
|
|
35
|
+
getCurrentUser: <TUser extends User>() =>
|
|
36
|
+
Promise.resolve({ username: 'a', roles: ['Role1'] } as unknown as TUser),
|
|
37
|
+
isAuthenticated: () => Promise.resolve(true),
|
|
38
|
+
}))
|
|
39
|
+
const exampleAction = vi.fn(async () => EmptyResult())
|
|
40
|
+
const authorized = Authorize('Role2')(exampleAction)
|
|
41
41
|
|
|
42
42
|
await expect(() => authorized({ injector: i, request, response })).rejects.toThrow(AuthorizationError)
|
|
43
|
-
expect(
|
|
43
|
+
expect(exampleAction).not.toHaveBeenCalled()
|
|
44
44
|
})
|
|
45
45
|
})
|
|
46
46
|
|
|
47
|
-
it('
|
|
48
|
-
await usingAsync(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
)
|
|
57
|
-
const
|
|
58
|
-
const authorized = Authorize('Role1')(exampleAuthorizedAction)
|
|
59
|
-
const params = { injector: i, body: undefined, query: undefined, request, response }
|
|
47
|
+
it('invokes the wrapped action when the current user holds the required role', async () => {
|
|
48
|
+
await usingAsync(createInjector(), async (i) => {
|
|
49
|
+
i.bind(IdentityContext, () => ({
|
|
50
|
+
isAuthorized: () => Promise.resolve(true),
|
|
51
|
+
getCurrentUser: <TUser extends User>() =>
|
|
52
|
+
Promise.resolve({ username: 'a', roles: ['Role1'] } as unknown as TUser),
|
|
53
|
+
isAuthenticated: () => Promise.resolve(true),
|
|
54
|
+
}))
|
|
55
|
+
const exampleAction = vi.fn(async () => EmptyResult())
|
|
56
|
+
const authorized = Authorize('Role1')(exampleAction)
|
|
57
|
+
const params = { injector: i, request, response }
|
|
60
58
|
const result = await authorized(params)
|
|
61
59
|
expect(result.statusCode).toBe(200)
|
|
62
60
|
expect(result.chunk).toBe(undefined)
|
|
63
|
-
expect(
|
|
61
|
+
expect(exampleAction).toHaveBeenCalledWith(params)
|
|
64
62
|
})
|
|
65
63
|
})
|
|
66
64
|
})
|
package/src/authorize.ts
CHANGED
|
@@ -1,25 +1,14 @@
|
|
|
1
1
|
import { AuthorizationError, isAuthorized } from '@furystack/core'
|
|
2
2
|
import { sleepAsync } from '@furystack/utils'
|
|
3
|
+
import type { Authenticate } from './authenticate.js'
|
|
3
4
|
import type { ActionResult, RequestAction, RequestActionOptions } from './request-action-implementation.js'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* @returns A function that wraps the provided action with authorization check
|
|
12
|
-
* @example
|
|
13
|
-
* ```ts
|
|
14
|
-
* // Require 'admin' role
|
|
15
|
-
* const adminEndpoint = Authorize('admin')(myEndpoint)
|
|
16
|
-
*
|
|
17
|
-
* // Require multiple roles (user must have ALL specified roles)
|
|
18
|
-
* const superAdminEndpoint = Authorize('admin', 'superuser')(myEndpoint)
|
|
19
|
-
*
|
|
20
|
-
* // Chain with Authenticate for authentication + authorization
|
|
21
|
-
* const protectedEndpoint = Authenticate()(Authorize('admin')(myEndpoint))
|
|
22
|
-
* ```
|
|
7
|
+
* Wraps a {@link RequestAction} to require **all** specified roles. Throws
|
|
8
|
+
* {@link AuthorizationError} (translated to 403 by `ErrorAction`) when any
|
|
9
|
+
* role is missing. Adds a 0–1 s random delay on rejection to mitigate
|
|
10
|
+
* timing attacks. Compose with {@link Authenticate} to require both:
|
|
11
|
+
* `Authenticate()(Authorize('admin')(myEndpoint))`.
|
|
23
12
|
*/
|
|
24
13
|
export const Authorize =
|
|
25
14
|
(...roles: string[]) =>
|