;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.onpreroutingtoolkit.md b/docs/development/core/server/kibana-plugin-core-server.onpreroutingtoolkit.md
new file mode 100644
index 00000000000000..c564896b46a27e
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.onpreroutingtoolkit.md
@@ -0,0 +1,21 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [OnPreRoutingToolkit](./kibana-plugin-core-server.onpreroutingtoolkit.md)
+
+## OnPreRoutingToolkit interface
+
+A tool set defining an outcome of OnPreRouting interceptor for incoming request.
+
+Signature:
+
+```typescript
+export interface OnPreRoutingToolkit
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [next](./kibana-plugin-core-server.onpreroutingtoolkit.next.md) | () => OnPreRoutingResult
| To pass request to the next handler |
+| [rewriteUrl](./kibana-plugin-core-server.onpreroutingtoolkit.rewriteurl.md) | (url: string) => OnPreRoutingResult
| Rewrite requested resources url before is was authenticated and routed to a handler |
+
diff --git a/docs/development/core/server/kibana-plugin-core-server.onpreroutingtoolkit.next.md b/docs/development/core/server/kibana-plugin-core-server.onpreroutingtoolkit.next.md
new file mode 100644
index 00000000000000..7fb0b2ce67ba5d
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.onpreroutingtoolkit.next.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [OnPreRoutingToolkit](./kibana-plugin-core-server.onpreroutingtoolkit.md) > [next](./kibana-plugin-core-server.onpreroutingtoolkit.next.md)
+
+## OnPreRoutingToolkit.next property
+
+To pass request to the next handler
+
+Signature:
+
+```typescript
+next: () => OnPreRoutingResult;
+```
diff --git a/docs/development/core/server/kibana-plugin-core-server.onpreroutingtoolkit.rewriteurl.md b/docs/development/core/server/kibana-plugin-core-server.onpreroutingtoolkit.rewriteurl.md
new file mode 100644
index 00000000000000..346a12711c7238
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.onpreroutingtoolkit.rewriteurl.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [OnPreRoutingToolkit](./kibana-plugin-core-server.onpreroutingtoolkit.md) > [rewriteUrl](./kibana-plugin-core-server.onpreroutingtoolkit.rewriteurl.md)
+
+## OnPreRoutingToolkit.rewriteUrl property
+
+Rewrite requested resources url before is was authenticated and routed to a handler
+
+Signature:
+
+```typescript
+rewriteUrl: (url: string) => OnPreRoutingResult;
+```
diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts
index bbef0a105c0896..7d37af833d4c17 100644
--- a/src/core/server/http/http_server.mocks.ts
+++ b/src/core/server/http/http_server.mocks.ts
@@ -33,7 +33,7 @@ import {
} from './router';
import { OnPreResponseToolkit } from './lifecycle/on_pre_response';
import { OnPostAuthToolkit } from './lifecycle/on_post_auth';
-import { OnPreAuthToolkit } from './lifecycle/on_pre_auth';
+import { OnPreRoutingToolkit } from './lifecycle/on_pre_routing';
interface RequestFixtureOptions {
auth?: { isAuthenticated: boolean };
@@ -161,7 +161,7 @@ const createLifecycleResponseFactoryMock = (): jest.Mocked;
+type ToolkitMock = jest.Mocked;
const createToolkitMock = (): ToolkitMock => {
return {
diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts
index 72cb0b2821c5c2..601eba835a54e8 100644
--- a/src/core/server/http/http_server.test.ts
+++ b/src/core/server/http/http_server.test.ts
@@ -1089,6 +1089,16 @@ describe('setup contract', () => {
});
});
+ describe('#registerOnPreRouting', () => {
+ test('does not throw if called after stop', async () => {
+ const { registerOnPreRouting } = await server.setup(config);
+ await server.stop();
+ expect(() => {
+ registerOnPreRouting((req, res) => res.unauthorized());
+ }).not.toThrow();
+ });
+ });
+
describe('#registerOnPreAuth', () => {
test('does not throw if called after stop', async () => {
const { registerOnPreAuth } = await server.setup(config);
diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts
index 1abf5c0c133bbe..9c16162d693348 100644
--- a/src/core/server/http/http_server.ts
+++ b/src/core/server/http/http_server.ts
@@ -24,8 +24,9 @@ import { Logger, LoggerFactory } from '../logging';
import { HttpConfig } from './http_config';
import { createServer, getListenerOptions, getServerOptions } from './http_tools';
import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth';
+import { adoptToHapiOnPreAuth, OnPreAuthHandler } from './lifecycle/on_pre_auth';
import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth';
-import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth';
+import { adoptToHapiOnRequest, OnPreRoutingHandler } from './lifecycle/on_pre_routing';
import { adoptToHapiOnPreResponseFormat, OnPreResponseHandler } from './lifecycle/on_pre_response';
import { IRouter, RouteConfigOptions, KibanaRouteState, isSafeMethod } from './router';
import {
@@ -49,8 +50,9 @@ export interface HttpServerSetup {
basePath: HttpServiceSetup['basePath'];
csp: HttpServiceSetup['csp'];
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
- registerAuth: HttpServiceSetup['registerAuth'];
+ registerOnPreRouting: HttpServiceSetup['registerOnPreRouting'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
+ registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
registerOnPreResponse: HttpServiceSetup['registerOnPreResponse'];
getAuthHeaders: GetAuthHeaders;
@@ -64,7 +66,11 @@ export interface HttpServerSetup {
/** @internal */
export type LifecycleRegistrar = Pick<
HttpServerSetup,
- 'registerAuth' | 'registerOnPreAuth' | 'registerOnPostAuth' | 'registerOnPreResponse'
+ | 'registerOnPreRouting'
+ | 'registerOnPreAuth'
+ | 'registerAuth'
+ | 'registerOnPostAuth'
+ | 'registerOnPreResponse'
>;
export class HttpServer {
@@ -113,12 +119,13 @@ export class HttpServer {
return {
registerRouter: this.registerRouter.bind(this),
registerStaticDir: this.registerStaticDir.bind(this),
+ registerOnPreRouting: this.registerOnPreRouting.bind(this),
registerOnPreAuth: this.registerOnPreAuth.bind(this),
+ registerAuth: this.registerAuth.bind(this),
registerOnPostAuth: this.registerOnPostAuth.bind(this),
registerOnPreResponse: this.registerOnPreResponse.bind(this),
createCookieSessionStorageFactory: (cookieOptions: SessionStorageCookieOptions) =>
this.createCookieSessionStorageFactory(cookieOptions, config.basePath),
- registerAuth: this.registerAuth.bind(this),
basePath: basePathService,
csp: config.csp,
auth: {
@@ -222,7 +229,7 @@ export class HttpServer {
return;
}
- this.registerOnPreAuth((request, response, toolkit) => {
+ this.registerOnPreRouting((request, response, toolkit) => {
const oldUrl = request.url.href!;
const newURL = basePathService.remove(oldUrl);
const shouldRedirect = newURL !== oldUrl;
@@ -263,6 +270,17 @@ export class HttpServer {
}
}
+ private registerOnPreAuth(fn: OnPreAuthHandler) {
+ if (this.server === undefined) {
+ throw new Error('Server is not created yet');
+ }
+ if (this.stopped) {
+ this.log.warn(`registerOnPreAuth called after stop`);
+ }
+
+ this.server.ext('onPreAuth', adoptToHapiOnPreAuth(fn, this.log));
+ }
+
private registerOnPostAuth(fn: OnPostAuthHandler) {
if (this.server === undefined) {
throw new Error('Server is not created yet');
@@ -274,15 +292,15 @@ export class HttpServer {
this.server.ext('onPostAuth', adoptToHapiOnPostAuthFormat(fn, this.log));
}
- private registerOnPreAuth(fn: OnPreAuthHandler) {
+ private registerOnPreRouting(fn: OnPreRoutingHandler) {
if (this.server === undefined) {
throw new Error('Server is not created yet');
}
if (this.stopped) {
- this.log.warn(`registerOnPreAuth called after stop`);
+ this.log.warn(`registerOnPreRouting called after stop`);
}
- this.server.ext('onRequest', adoptToHapiOnPreAuthFormat(fn, this.log));
+ this.server.ext('onRequest', adoptToHapiOnRequest(fn, this.log));
}
private registerOnPreResponse(fn: OnPreResponseHandler) {
diff --git a/src/core/server/http/http_service.mock.ts b/src/core/server/http/http_service.mock.ts
index 5e7ee7b658ecae..51f11b15f2e097 100644
--- a/src/core/server/http/http_service.mock.ts
+++ b/src/core/server/http/http_service.mock.ts
@@ -29,7 +29,7 @@ import {
} from './types';
import { HttpService } from './http_service';
import { AuthStatus } from './auth_state_storage';
-import { OnPreAuthToolkit } from './lifecycle/on_pre_auth';
+import { OnPreRoutingToolkit } from './lifecycle/on_pre_routing';
import { AuthToolkit } from './lifecycle/auth';
import { sessionStorageMock } from './cookie_session_storage.mocks';
import { OnPostAuthToolkit } from './lifecycle/on_post_auth';
@@ -87,6 +87,7 @@ const createInternalSetupContractMock = () => {
config: jest.fn().mockReturnValue(configMock.create()),
} as unknown) as jest.MockedClass,
createCookieSessionStorageFactory: jest.fn(),
+ registerOnPreRouting: jest.fn(),
registerOnPreAuth: jest.fn(),
registerAuth: jest.fn(),
registerOnPostAuth: jest.fn(),
@@ -117,7 +118,8 @@ const createSetupContractMock = () => {
const mock: HttpServiceSetupMock = {
createCookieSessionStorageFactory: internalMock.createCookieSessionStorageFactory,
- registerOnPreAuth: internalMock.registerOnPreAuth,
+ registerOnPreRouting: internalMock.registerOnPreRouting,
+ registerOnPreAuth: jest.fn(),
registerAuth: internalMock.registerAuth,
registerOnPostAuth: internalMock.registerOnPostAuth,
registerOnPreResponse: internalMock.registerOnPreResponse,
@@ -173,7 +175,7 @@ const createHttpServiceMock = () => {
return mocked;
};
-const createOnPreAuthToolkitMock = (): jest.Mocked => ({
+const createOnPreAuthToolkitMock = (): jest.Mocked => ({
next: jest.fn(),
rewriteUrl: jest.fn(),
});
diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts
index 65d633260a7911..e91f7d93758429 100644
--- a/src/core/server/http/index.ts
+++ b/src/core/server/http/index.ts
@@ -64,7 +64,7 @@ export {
SafeRouteMethod,
} from './router';
export { BasePathProxyServer } from './base_path_proxy_server';
-export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth';
+export { OnPreRoutingHandler, OnPreRoutingToolkit } from './lifecycle/on_pre_routing';
export {
AuthenticationHandler,
AuthHeaders,
@@ -78,6 +78,7 @@ export {
AuthResultType,
} from './lifecycle/auth';
export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth';
+export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth';
export {
OnPreResponseHandler,
OnPreResponseToolkit,
diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts
index 0ee53a04d9f87d..3c5f22500e5e04 100644
--- a/src/core/server/http/integration_tests/core_services.test.ts
+++ b/src/core/server/http/integration_tests/core_services.test.ts
@@ -337,7 +337,7 @@ describe('http service', () => {
it('basePath information for an incoming request is available in legacy server', async () => {
const reqBasePath = '/requests-specific-base-path';
const { http } = await root.setup();
- http.registerOnPreAuth((req, res, toolkit) => {
+ http.registerOnPreRouting((req, res, toolkit) => {
http.basePath.set(req, reqBasePath);
return toolkit.next();
});
diff --git a/src/core/server/http/integration_tests/lifecycle.test.ts b/src/core/server/http/integration_tests/lifecycle.test.ts
index cbab14115ba6b0..b9548bf7a8d707 100644
--- a/src/core/server/http/integration_tests/lifecycle.test.ts
+++ b/src/core/server/http/integration_tests/lifecycle.test.ts
@@ -57,20 +57,22 @@ interface StorageData {
expires: number;
}
-describe('OnPreAuth', () => {
+describe('OnPreRouting', () => {
it('supports registering a request interceptor', async () => {
- const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(setupDeps);
+ const { registerOnPreRouting, server: innerServer, createRouter } = await server.setup(
+ setupDeps
+ );
const router = createRouter('/');
router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: 'ok' }));
const callingOrder: string[] = [];
- registerOnPreAuth((req, res, t) => {
+ registerOnPreRouting((req, res, t) => {
callingOrder.push('first');
return t.next();
});
- registerOnPreAuth((req, res, t) => {
+ registerOnPreRouting((req, res, t) => {
callingOrder.push('second');
return t.next();
});
@@ -82,7 +84,9 @@ describe('OnPreAuth', () => {
});
it('supports request forwarding to specified url', async () => {
- const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(setupDeps);
+ const { registerOnPreRouting, server: innerServer, createRouter } = await server.setup(
+ setupDeps
+ );
const router = createRouter('/');
router.get({ path: '/initial', validate: false }, (context, req, res) =>
@@ -93,13 +97,13 @@ describe('OnPreAuth', () => {
);
let urlBeforeForwarding;
- registerOnPreAuth((req, res, t) => {
+ registerOnPreRouting((req, res, t) => {
urlBeforeForwarding = ensureRawRequest(req).raw.req.url;
return t.rewriteUrl('/redirectUrl');
});
let urlAfterForwarding;
- registerOnPreAuth((req, res, t) => {
+ registerOnPreRouting((req, res, t) => {
// used by legacy platform
urlAfterForwarding = ensureRawRequest(req).raw.req.url;
return t.next();
@@ -113,6 +117,152 @@ describe('OnPreAuth', () => {
expect(urlAfterForwarding).toBe('/redirectUrl');
});
+ it('supports redirection from the interceptor', async () => {
+ const { registerOnPreRouting, server: innerServer, createRouter } = await server.setup(
+ setupDeps
+ );
+ const router = createRouter('/');
+
+ const redirectUrl = '/redirectUrl';
+ router.get({ path: '/initial', validate: false }, (context, req, res) => res.ok());
+
+ registerOnPreRouting((req, res, t) =>
+ res.redirected({
+ headers: {
+ location: redirectUrl,
+ },
+ })
+ );
+ await server.start();
+
+ const result = await supertest(innerServer.listener).get('/initial').expect(302);
+
+ expect(result.header.location).toBe(redirectUrl);
+ });
+
+ it('supports rejecting request and adjusting response headers', async () => {
+ const { registerOnPreRouting, server: innerServer, createRouter } = await server.setup(
+ setupDeps
+ );
+ const router = createRouter('/');
+
+ router.get({ path: '/', validate: false }, (context, req, res) => res.ok());
+
+ registerOnPreRouting((req, res, t) =>
+ res.unauthorized({
+ headers: {
+ 'www-authenticate': 'challenge',
+ },
+ })
+ );
+ await server.start();
+
+ const result = await supertest(innerServer.listener).get('/').expect(401);
+
+ expect(result.header['www-authenticate']).toBe('challenge');
+ });
+
+ it('does not expose error details if interceptor throws', async () => {
+ const { registerOnPreRouting, server: innerServer, createRouter } = await server.setup(
+ setupDeps
+ );
+ const router = createRouter('/');
+
+ router.get({ path: '/', validate: false }, (context, req, res) => res.ok());
+
+ registerOnPreRouting((req, res, t) => {
+ throw new Error('reason');
+ });
+ await server.start();
+
+ const result = await supertest(innerServer.listener).get('/').expect(500);
+
+ expect(result.body.message).toBe('An internal server error occurred.');
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
+ Array [
+ Array [
+ [Error: reason],
+ ],
+ ]
+ `);
+ });
+
+ it('returns internal error if interceptor returns unexpected result', async () => {
+ const { registerOnPreRouting, server: innerServer, createRouter } = await server.setup(
+ setupDeps
+ );
+ const router = createRouter('/');
+
+ router.get({ path: '/', validate: false }, (context, req, res) => res.ok());
+
+ registerOnPreRouting((req, res, t) => ({} as any));
+ await server.start();
+
+ const result = await supertest(innerServer.listener).get('/').expect(500);
+
+ expect(result.body.message).toBe('An internal server error occurred.');
+ expect(loggingSystemMock.collect(logger).error).toMatchInlineSnapshot(`
+ Array [
+ Array [
+ [Error: Unexpected result from OnPreRouting. Expected OnPreRoutingResult or KibanaResponse, but given: [object Object].],
+ ],
+ ]
+ `);
+ });
+
+ it(`doesn't share request object between interceptors`, async () => {
+ const { registerOnPreRouting, server: innerServer, createRouter } = await server.setup(
+ setupDeps
+ );
+ const router = createRouter('/');
+
+ registerOnPreRouting((req, res, t) => {
+ // don't complain customField is not defined on Request type
+ (req as any).customField = { value: 42 };
+ return t.next();
+ });
+ registerOnPreRouting((req, res, t) => {
+ // don't complain customField is not defined on Request type
+ if (typeof (req as any).customField !== 'undefined') {
+ throw new Error('Request object was mutated');
+ }
+ return t.next();
+ });
+ router.get({ path: '/', validate: false }, (context, req, res) =>
+ // don't complain customField is not defined on Request type
+ res.ok({ body: { customField: String((req as any).customField) } })
+ );
+
+ await server.start();
+
+ await supertest(innerServer.listener).get('/').expect(200, { customField: 'undefined' });
+ });
+});
+
+describe('OnPreAuth', () => {
+ it('supports registering a request interceptor', async () => {
+ const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(setupDeps);
+ const router = createRouter('/');
+
+ router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: 'ok' }));
+
+ const callingOrder: string[] = [];
+ registerOnPreAuth((req, res, t) => {
+ callingOrder.push('first');
+ return t.next();
+ });
+
+ registerOnPreAuth((req, res, t) => {
+ callingOrder.push('second');
+ return t.next();
+ });
+ await server.start();
+
+ await supertest(innerServer.listener).get('/').expect(200, 'ok');
+
+ expect(callingOrder).toEqual(['first', 'second']);
+ });
+
it('supports redirection from the interceptor', async () => {
const { registerOnPreAuth, server: innerServer, createRouter } = await server.setup(setupDeps);
const router = createRouter('/');
@@ -203,20 +353,20 @@ describe('OnPreAuth', () => {
const router = createRouter('/');
registerOnPreAuth((req, res, t) => {
- // don't complain customField is not defined on Request type
- (req as any).customField = { value: 42 };
+ // @ts-expect-error customField property is not defined on request object
+ req.customField = { value: 42 };
return t.next();
});
registerOnPreAuth((req, res, t) => {
- // don't complain customField is not defined on Request type
- if (typeof (req as any).customField !== 'undefined') {
+ // @ts-expect-error customField property is not defined on request object
+ if (typeof req.customField !== 'undefined') {
throw new Error('Request object was mutated');
}
return t.next();
});
router.get({ path: '/', validate: false }, (context, req, res) =>
- // don't complain customField is not defined on Request type
- res.ok({ body: { customField: String((req as any).customField) } })
+ // @ts-expect-error customField property is not defined on request object
+ res.ok({ body: { customField: String(req.customField) } })
);
await server.start();
@@ -664,7 +814,7 @@ describe('Auth', () => {
it.skip('is the only place with access to the authorization header', async () => {
const {
- registerOnPreAuth,
+ registerOnPreRouting,
registerAuth,
registerOnPostAuth,
server: innerServer,
@@ -672,9 +822,9 @@ describe('Auth', () => {
} = await server.setup(setupDeps);
const router = createRouter('/');
- let fromRegisterOnPreAuth;
- await registerOnPreAuth((req, res, toolkit) => {
- fromRegisterOnPreAuth = req.headers.authorization;
+ let fromregisterOnPreRouting;
+ await registerOnPreRouting((req, res, toolkit) => {
+ fromregisterOnPreRouting = req.headers.authorization;
return toolkit.next();
});
@@ -701,7 +851,7 @@ describe('Auth', () => {
const token = 'Basic: user:password';
await supertest(innerServer.listener).get('/').set('Authorization', token).expect(200);
- expect(fromRegisterOnPreAuth).toEqual({});
+ expect(fromregisterOnPreRouting).toEqual({});
expect(fromRegisterAuth).toEqual({ authorization: token });
expect(fromRegisterOnPostAuth).toEqual({});
expect(fromRouteHandler).toEqual({});
@@ -1137,3 +1287,135 @@ describe('OnPreResponse', () => {
expect(requestBody).toStrictEqual({});
});
});
+
+describe('run interceptors in the right order', () => {
+ it('with Auth registered', async () => {
+ const {
+ registerOnPreRouting,
+ registerOnPreAuth,
+ registerAuth,
+ registerOnPostAuth,
+ registerOnPreResponse,
+ server: innerServer,
+ createRouter,
+ } = await server.setup(setupDeps);
+
+ const router = createRouter('/');
+
+ const executionOrder: string[] = [];
+ registerOnPreRouting((req, res, t) => {
+ executionOrder.push('onPreRouting');
+ return t.next();
+ });
+ registerOnPreAuth((req, res, t) => {
+ executionOrder.push('onPreAuth');
+ return t.next();
+ });
+ registerAuth((req, res, t) => {
+ executionOrder.push('auth');
+ return t.authenticated({});
+ });
+ registerOnPostAuth((req, res, t) => {
+ executionOrder.push('onPostAuth');
+ return t.next();
+ });
+ registerOnPreResponse((req, res, t) => {
+ executionOrder.push('onPreResponse');
+ return t.next();
+ });
+
+ router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: 'ok' }));
+
+ await server.start();
+
+ await supertest(innerServer.listener).get('/').expect(200);
+ expect(executionOrder).toEqual([
+ 'onPreRouting',
+ 'onPreAuth',
+ 'auth',
+ 'onPostAuth',
+ 'onPreResponse',
+ ]);
+ });
+
+ it('with no Auth registered', async () => {
+ const {
+ registerOnPreRouting,
+ registerOnPreAuth,
+ registerOnPostAuth,
+ registerOnPreResponse,
+ server: innerServer,
+ createRouter,
+ } = await server.setup(setupDeps);
+
+ const router = createRouter('/');
+
+ const executionOrder: string[] = [];
+ registerOnPreRouting((req, res, t) => {
+ executionOrder.push('onPreRouting');
+ return t.next();
+ });
+ registerOnPreAuth((req, res, t) => {
+ executionOrder.push('onPreAuth');
+ return t.next();
+ });
+ registerOnPostAuth((req, res, t) => {
+ executionOrder.push('onPostAuth');
+ return t.next();
+ });
+ registerOnPreResponse((req, res, t) => {
+ executionOrder.push('onPreResponse');
+ return t.next();
+ });
+
+ router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: 'ok' }));
+
+ await server.start();
+
+ await supertest(innerServer.listener).get('/').expect(200);
+ expect(executionOrder).toEqual(['onPreRouting', 'onPreAuth', 'onPostAuth', 'onPreResponse']);
+ });
+
+ it('when a user failed auth', async () => {
+ const {
+ registerOnPreRouting,
+ registerOnPreAuth,
+ registerOnPostAuth,
+ registerAuth,
+ registerOnPreResponse,
+ server: innerServer,
+ createRouter,
+ } = await server.setup(setupDeps);
+
+ const router = createRouter('/');
+
+ const executionOrder: string[] = [];
+ registerOnPreRouting((req, res, t) => {
+ executionOrder.push('onPreRouting');
+ return t.next();
+ });
+ registerOnPreAuth((req, res, t) => {
+ executionOrder.push('onPreAuth');
+ return t.next();
+ });
+ registerAuth((req, res, t) => {
+ executionOrder.push('auth');
+ return res.forbidden();
+ });
+ registerOnPostAuth((req, res, t) => {
+ executionOrder.push('onPostAuth');
+ return t.next();
+ });
+ registerOnPreResponse((req, res, t) => {
+ executionOrder.push('onPreResponse');
+ return t.next();
+ });
+
+ router.get({ path: '/', validate: false }, (context, req, res) => res.ok({ body: 'ok' }));
+
+ await server.start();
+
+ await supertest(innerServer.listener).get('/').expect(403);
+ expect(executionOrder).toEqual(['onPreRouting', 'onPreAuth', 'auth', 'onPreResponse']);
+ });
+});
diff --git a/src/core/server/http/lifecycle/on_pre_auth.ts b/src/core/server/http/lifecycle/on_pre_auth.ts
index dc2ae6922fb94a..f76fe87fd14a3a 100644
--- a/src/core/server/http/lifecycle/on_pre_auth.ts
+++ b/src/core/server/http/lifecycle/on_pre_auth.ts
@@ -29,33 +29,21 @@ import {
enum ResultType {
next = 'next',
- rewriteUrl = 'rewriteUrl',
}
interface Next {
type: ResultType.next;
}
-interface RewriteUrl {
- type: ResultType.rewriteUrl;
- url: string;
-}
-
-type OnPreAuthResult = Next | RewriteUrl;
+type OnPreAuthResult = Next;
const preAuthResult = {
next(): OnPreAuthResult {
return { type: ResultType.next };
},
- rewriteUrl(url: string): OnPreAuthResult {
- return { type: ResultType.rewriteUrl, url };
- },
isNext(result: OnPreAuthResult): result is Next {
return result && result.type === ResultType.next;
},
- isRewriteUrl(result: OnPreAuthResult): result is RewriteUrl {
- return result && result.type === ResultType.rewriteUrl;
- },
};
/**
@@ -65,13 +53,10 @@ const preAuthResult = {
export interface OnPreAuthToolkit {
/** To pass request to the next handler */
next: () => OnPreAuthResult;
- /** Rewrite requested resources url before is was authenticated and routed to a handler */
- rewriteUrl: (url: string) => OnPreAuthResult;
}
const toolkit: OnPreAuthToolkit = {
next: preAuthResult.next,
- rewriteUrl: preAuthResult.rewriteUrl,
};
/**
@@ -88,9 +73,9 @@ export type OnPreAuthHandler = (
* @public
* Adopt custom request interceptor to Hapi lifecycle system.
* @param fn - an extension point allowing to perform custom logic for
- * incoming HTTP requests.
+ * incoming HTTP requests before a user has been authenticated.
*/
-export function adoptToHapiOnPreAuthFormat(fn: OnPreAuthHandler, log: Logger) {
+export function adoptToHapiOnPreAuth(fn: OnPreAuthHandler, log: Logger) {
return async function interceptPreAuthRequest(
request: Request,
responseToolkit: HapiResponseToolkit
@@ -107,13 +92,6 @@ export function adoptToHapiOnPreAuthFormat(fn: OnPreAuthHandler, log: Logger) {
return responseToolkit.continue;
}
- if (preAuthResult.isRewriteUrl(result)) {
- const { url } = result;
- request.setUrl(url);
- // We should update raw request as well since it can be proxied to the old platform
- request.raw.req.url = url;
- return responseToolkit.continue;
- }
throw new Error(
`Unexpected result from OnPreAuth. Expected OnPreAuthResult or KibanaResponse, but given: ${result}.`
);
diff --git a/src/core/server/http/lifecycle/on_pre_response.ts b/src/core/server/http/lifecycle/on_pre_response.ts
index 9c8c6fba690d18..4d1b53313a51fd 100644
--- a/src/core/server/http/lifecycle/on_pre_response.ts
+++ b/src/core/server/http/lifecycle/on_pre_response.ts
@@ -64,7 +64,7 @@ const preResponseResult = {
};
/**
- * A tool set defining an outcome of OnPreAuth interceptor for incoming request.
+ * A tool set defining an outcome of OnPreResponse interceptor for incoming request.
* @public
*/
export interface OnPreResponseToolkit {
@@ -77,7 +77,7 @@ const toolkit: OnPreResponseToolkit = {
};
/**
- * See {@link OnPreAuthToolkit}.
+ * See {@link OnPreRoutingToolkit}.
* @public
*/
export type OnPreResponseHandler = (
diff --git a/src/core/server/http/lifecycle/on_pre_routing.ts b/src/core/server/http/lifecycle/on_pre_routing.ts
new file mode 100644
index 00000000000000..e62eb54f2398f5
--- /dev/null
+++ b/src/core/server/http/lifecycle/on_pre_routing.ts
@@ -0,0 +1,125 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { Lifecycle, Request, ResponseToolkit as HapiResponseToolkit } from 'hapi';
+import { Logger } from '../../logging';
+import {
+ HapiResponseAdapter,
+ KibanaRequest,
+ KibanaResponse,
+ lifecycleResponseFactory,
+ LifecycleResponseFactory,
+} from '../router';
+
+enum ResultType {
+ next = 'next',
+ rewriteUrl = 'rewriteUrl',
+}
+
+interface Next {
+ type: ResultType.next;
+}
+
+interface RewriteUrl {
+ type: ResultType.rewriteUrl;
+ url: string;
+}
+
+type OnPreRoutingResult = Next | RewriteUrl;
+
+const preRoutingResult = {
+ next(): OnPreRoutingResult {
+ return { type: ResultType.next };
+ },
+ rewriteUrl(url: string): OnPreRoutingResult {
+ return { type: ResultType.rewriteUrl, url };
+ },
+ isNext(result: OnPreRoutingResult): result is Next {
+ return result && result.type === ResultType.next;
+ },
+ isRewriteUrl(result: OnPreRoutingResult): result is RewriteUrl {
+ return result && result.type === ResultType.rewriteUrl;
+ },
+};
+
+/**
+ * @public
+ * A tool set defining an outcome of OnPreRouting interceptor for incoming request.
+ */
+export interface OnPreRoutingToolkit {
+ /** To pass request to the next handler */
+ next: () => OnPreRoutingResult;
+ /** Rewrite requested resources url before is was authenticated and routed to a handler */
+ rewriteUrl: (url: string) => OnPreRoutingResult;
+}
+
+const toolkit: OnPreRoutingToolkit = {
+ next: preRoutingResult.next,
+ rewriteUrl: preRoutingResult.rewriteUrl,
+};
+
+/**
+ * See {@link OnPreRoutingToolkit}.
+ * @public
+ */
+export type OnPreRoutingHandler = (
+ request: KibanaRequest,
+ response: LifecycleResponseFactory,
+ toolkit: OnPreRoutingToolkit
+) => OnPreRoutingResult | KibanaResponse | Promise;
+
+/**
+ * @public
+ * Adopt custom request interceptor to Hapi lifecycle system.
+ * @param fn - an extension point allowing to perform custom logic for
+ * incoming HTTP requests.
+ */
+export function adoptToHapiOnRequest(fn: OnPreRoutingHandler, log: Logger) {
+ return async function interceptPreRoutingRequest(
+ request: Request,
+ responseToolkit: HapiResponseToolkit
+ ): Promise {
+ const hapiResponseAdapter = new HapiResponseAdapter(responseToolkit);
+
+ try {
+ const result = await fn(KibanaRequest.from(request), lifecycleResponseFactory, toolkit);
+ if (result instanceof KibanaResponse) {
+ return hapiResponseAdapter.handle(result);
+ }
+
+ if (preRoutingResult.isNext(result)) {
+ return responseToolkit.continue;
+ }
+
+ if (preRoutingResult.isRewriteUrl(result)) {
+ const { url } = result;
+ request.setUrl(url);
+ // We should update raw request as well since it can be proxied to the old platform
+ request.raw.req.url = url;
+ return responseToolkit.continue;
+ }
+ throw new Error(
+ `Unexpected result from OnPreRouting. Expected OnPreRoutingResult or KibanaResponse, but given: ${result}.`
+ );
+ } catch (error) {
+ log.error(error);
+ return hapiResponseAdapter.toInternalError();
+ }
+ };
+}
diff --git a/src/core/server/http/types.ts b/src/core/server/http/types.ts
index 241af1a3020cba..3df098a1df00d6 100644
--- a/src/core/server/http/types.ts
+++ b/src/core/server/http/types.ts
@@ -25,6 +25,7 @@ import { HttpServerSetup } from './http_server';
import { SessionStorageCookieOptions } from './cookie_session_storage';
import { SessionStorageFactory } from './session_storage';
import { AuthenticationHandler } from './lifecycle/auth';
+import { OnPreRoutingHandler } from './lifecycle/on_pre_routing';
import { OnPreAuthHandler } from './lifecycle/on_pre_auth';
import { OnPostAuthHandler } from './lifecycle/on_post_auth';
import { OnPreResponseHandler } from './lifecycle/on_pre_response';
@@ -145,15 +146,26 @@ export interface HttpServiceSetup {
) => Promise>;
/**
- * To define custom logic to perform for incoming requests.
+ * To define custom logic to perform for incoming requests before server performs a route lookup.
*
* @remarks
- * Runs the handler before Auth interceptor performs a check that user has access to requested resources, so it's the
- * only place when you can forward a request to another URL right on the server.
- * Can register any number of registerOnPostAuth, which are called in sequence
+ * It's the only place when you can forward a request to another URL right on the server.
+ * Can register any number of registerOnPreRouting, which are called in sequence
+ * (from the first registered to the last). See {@link OnPreRoutingHandler}.
+ *
+ * @param handler {@link OnPreRoutingHandler} - function to call.
+ */
+ registerOnPreRouting: (handler: OnPreRoutingHandler) => void;
+
+ /**
+ * To define custom logic to perform for incoming requests before
+ * the Auth interceptor performs a check that user has access to requested resources.
+ *
+ * @remarks
+ * Can register any number of registerOnPreAuth, which are called in sequence
* (from the first registered to the last). See {@link OnPreAuthHandler}.
*
- * @param handler {@link OnPreAuthHandler} - function to call.
+ * @param handler {@link OnPreRoutingHandler} - function to call.
*/
registerOnPreAuth: (handler: OnPreAuthHandler) => void;
@@ -170,13 +182,11 @@ export interface HttpServiceSetup {
registerAuth: (handler: AuthenticationHandler) => void;
/**
- * To define custom logic to perform for incoming requests.
+ * To define custom logic after Auth interceptor did make sure a user has access to the requested resource.
*
* @remarks
- * Runs the handler after Auth interceptor
- * did make sure a user has access to the requested resource.
* The auth state is available at stage via http.auth.get(..)
- * Can register any number of registerOnPreAuth, which are called in sequence
+ * Can register any number of registerOnPostAuth, which are called in sequence
* (from the first registered to the last). See {@link OnPostAuthHandler}.
*
* @param handler {@link OnPostAuthHandler} - function to call.
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index dcaa5f23672149..706ec88c6ebfda 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -148,6 +148,8 @@ export {
LegacyRequest,
OnPreAuthHandler,
OnPreAuthToolkit,
+ OnPreRoutingHandler,
+ OnPreRoutingToolkit,
OnPostAuthHandler,
OnPostAuthToolkit,
OnPreResponseHandler,
diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts
index 6b34a4eb58319a..fada40e773f12b 100644
--- a/src/core/server/legacy/legacy_service.ts
+++ b/src/core/server/legacy/legacy_service.ts
@@ -301,6 +301,7 @@ export class LegacyService implements CoreService {
),
createRouter: () => router,
resources: setupDeps.core.httpResources.createRegistrar(router),
+ registerOnPreRouting: setupDeps.core.http.registerOnPreRouting,
registerOnPreAuth: setupDeps.core.http.registerOnPreAuth,
registerAuth: setupDeps.core.http.registerAuth,
registerOnPostAuth: setupDeps.core.http.registerOnPostAuth,
diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts
index a6dd13a12b5278..c17b8df8bb52c0 100644
--- a/src/core/server/plugins/plugin_context.ts
+++ b/src/core/server/plugins/plugin_context.ts
@@ -157,6 +157,7 @@ export function createPluginSetupContext(
),
createRouter: () => router,
resources: deps.httpResources.createRegistrar(router),
+ registerOnPreRouting: deps.http.registerOnPreRouting,
registerOnPreAuth: deps.http.registerOnPreAuth,
registerAuth: deps.http.registerAuth,
registerOnPostAuth: deps.http.registerOnPostAuth,
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 3d3e1905577d91..886544a4df317f 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -811,6 +811,7 @@ export interface HttpServiceSetup {
registerOnPostAuth: (handler: OnPostAuthHandler) => void;
registerOnPreAuth: (handler: OnPreAuthHandler) => void;
registerOnPreResponse: (handler: OnPreResponseHandler) => void;
+ registerOnPreRouting: (handler: OnPreRoutingHandler) => void;
registerRouteHandlerContext: (contextName: T, provider: RequestHandlerContextProvider) => RequestHandlerContextContainer;
}
@@ -1536,7 +1537,6 @@ export type OnPreAuthHandler = (request: KibanaRequest, response: LifecycleRespo
// @public
export interface OnPreAuthToolkit {
next: () => OnPreAuthResult;
- rewriteUrl: (url: string) => OnPreAuthResult;
}
// @public
@@ -1560,6 +1560,17 @@ export interface OnPreResponseToolkit {
next: (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult;
}
+// Warning: (ae-forgotten-export) The symbol "OnPreRoutingResult" needs to be exported by the entry point index.d.ts
+//
+// @public
+export type OnPreRoutingHandler = (request: KibanaRequest, response: LifecycleResponseFactory, toolkit: OnPreRoutingToolkit) => OnPreRoutingResult | KibanaResponse | Promise;
+
+// @public
+export interface OnPreRoutingToolkit {
+ next: () => OnPreRoutingResult;
+ rewriteUrl: (url: string) => OnPreRoutingResult;
+}
+
// @public
export interface OpsMetrics {
concurrent_connections: OpsServerMetrics['concurrent_connections'];
diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts
index 18e9da25576eba..4b3a5d662f12de 100644
--- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts
+++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_request_interceptor.ts
@@ -5,7 +5,7 @@
*/
import {
KibanaRequest,
- OnPreAuthToolkit,
+ OnPreRoutingToolkit,
LifecycleResponseFactory,
CoreSetup,
} from 'src/core/server';
@@ -18,10 +18,10 @@ export interface OnRequestInterceptorDeps {
http: CoreSetup['http'];
}
export function initSpacesOnRequestInterceptor({ http }: OnRequestInterceptorDeps) {
- http.registerOnPreAuth(async function spacesOnPreAuthHandler(
+ http.registerOnPreRouting(async function spacesOnPreRoutingHandler(
request: KibanaRequest,
response: LifecycleResponseFactory,
- toolkit: OnPreAuthToolkit
+ toolkit: OnPreRoutingToolkit
) {
const serverBasePath = http.basePath.serverBasePath;
const path = request.url.pathname;