;
```
diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md
index adcbe30d24bf5..e28950653b60d 100644
--- a/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md
+++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.authenticated.md
@@ -9,5 +9,5 @@ Authentication is successful with given credentials, allow request to pass throu
Signature:
```typescript
-authenticated: (state: object) => AuthResult;
+authenticated: (state?: object) => AuthResult;
```
diff --git a/docs/development/core/server/kibana-plugin-server.authtoolkit.md b/docs/development/core/server/kibana-plugin-server.authtoolkit.md
index 519de0e7ff276..f32f7076f0119 100644
--- a/docs/development/core/server/kibana-plugin-server.authtoolkit.md
+++ b/docs/development/core/server/kibana-plugin-server.authtoolkit.md
@@ -16,7 +16,7 @@ export interface AuthToolkit
| Property | Type | Description |
| --- | --- | --- |
-| [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) | (state: object) => AuthResult | Authentication is successful with given credentials, allow request to pass through |
+| [authenticated](./kibana-plugin-server.authtoolkit.authenticated.md) | (state?: object) => AuthResult | Authentication is successful with given credentials, allow request to pass through |
| [redirected](./kibana-plugin-server.authtoolkit.redirected.md) | (url: string) => AuthResult | Authentication requires to interrupt request handling and redirect to a configured url |
| [rejected](./kibana-plugin-server.authtoolkit.rejected.md) | (error: Error, options?: {
statusCode?: number;
}) => AuthResult | Authentication is unsuccessful, fail the request with specified error. |
diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.from.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.from.md
deleted file mode 100644
index 28f7c8eaa308e..0000000000000
--- a/docs/development/core/server/kibana-plugin-server.kibanarequest.from.md
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [from](./kibana-plugin-server.kibanarequest.from.md)
-
-## KibanaRequest.from() method
-
-Factory for creating requests. Validates the request before creating an instance of a KibanaRequest.
-
-Signature:
-
-```typescript
-static from(req: Request, routeSchemas?: RouteSchemas
): KibanaRequest
;
-```
-
-## Parameters
-
-| Parameter | Type | Description |
-| --- | --- | --- |
-| req | Request | |
-| routeSchemas | RouteSchemas<P, Q, B> | |
-
-Returns:
-
-`KibanaRequest
`
-
diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.md
index b14be4864ffe4..a09632febe531 100644
--- a/docs/development/core/server/kibana-plugin-server.kibanarequest.md
+++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.md
@@ -33,7 +33,5 @@ export declare class KibanaRequeststatic | Factory for creating requests. Validates the request before creating an instance of a KibanaRequest. |
| [getFilteredHeaders(headersToKeep)](./kibana-plugin-server.kibanarequest.getfilteredheaders.md) | | |
-| [unstable\_getIncomingMessage()](./kibana-plugin-server.kibanarequest.unstable_getincomingmessage.md) | | |
diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.unstable_getincomingmessage.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.unstable_getincomingmessage.md
deleted file mode 100644
index 96474eb669335..0000000000000
--- a/docs/development/core/server/kibana-plugin-server.kibanarequest.unstable_getincomingmessage.md
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [unstable\_getIncomingMessage](./kibana-plugin-server.kibanarequest.unstable_getincomingmessage.md)
-
-## KibanaRequest.unstable\_getIncomingMessage() method
-
-Signature:
-
-```typescript
-unstable_getIncomingMessage(): import("http").IncomingMessage;
-```
-Returns:
-
-`import("http").IncomingMessage`
-
diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md
index e2168e6471cc9..7b7d3a9f0662e 100644
--- a/docs/development/core/server/kibana-plugin-server.md
+++ b/docs/development/core/server/kibana-plugin-server.md
@@ -43,6 +43,8 @@ The plugin integrates with the core system via lifecycle events: `setup`
| [PluginsServiceSetup](./kibana-plugin-server.pluginsservicesetup.md) | |
| [PluginsServiceStart](./kibana-plugin-server.pluginsservicestart.md) | |
| [RouteConfigOptions](./kibana-plugin-server.routeconfigoptions.md) | Route specific configuration. |
+| [SessionStorage](./kibana-plugin-server.sessionstorage.md) | Provides an interface to store and retrieve data across requests. |
+| [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) | SessionStorage factory to bind one to an incoming request |
## Type Aliases
diff --git a/docs/development/core/server/kibana-plugin-server.sessionstorage.clear.md b/docs/development/core/server/kibana-plugin-server.sessionstorage.clear.md
new file mode 100644
index 0000000000000..1f5813e181548
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.sessionstorage.clear.md
@@ -0,0 +1,17 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorage](./kibana-plugin-server.sessionstorage.md) > [clear](./kibana-plugin-server.sessionstorage.clear.md)
+
+## SessionStorage.clear() method
+
+Clears current session.
+
+Signature:
+
+```typescript
+clear(): void;
+```
+Returns:
+
+`void`
+
diff --git a/docs/development/core/server/kibana-plugin-server.sessionstorage.get.md b/docs/development/core/server/kibana-plugin-server.sessionstorage.get.md
new file mode 100644
index 0000000000000..26c63884ee71a
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.sessionstorage.get.md
@@ -0,0 +1,17 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorage](./kibana-plugin-server.sessionstorage.md) > [get](./kibana-plugin-server.sessionstorage.get.md)
+
+## SessionStorage.get() method
+
+Retrieves session value from the session storage.
+
+Signature:
+
+```typescript
+get(): Promise;
+```
+Returns:
+
+`Promise`
+
diff --git a/docs/development/core/server/kibana-plugin-server.sessionstorage.md b/docs/development/core/server/kibana-plugin-server.sessionstorage.md
new file mode 100644
index 0000000000000..02e48c1dd3dc4
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.sessionstorage.md
@@ -0,0 +1,22 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorage](./kibana-plugin-server.sessionstorage.md)
+
+## SessionStorage interface
+
+Provides an interface to store and retrieve data across requests.
+
+Signature:
+
+```typescript
+export interface SessionStorage
+```
+
+## Methods
+
+| Method | Description |
+| --- | --- |
+| [clear()](./kibana-plugin-server.sessionstorage.clear.md) | Clears current session. |
+| [get()](./kibana-plugin-server.sessionstorage.get.md) | Retrieves session value from the session storage. |
+| [set(sessionValue)](./kibana-plugin-server.sessionstorage.set.md) | Puts current session value into the session storage. |
+
diff --git a/docs/development/core/server/kibana-plugin-server.sessionstorage.set.md b/docs/development/core/server/kibana-plugin-server.sessionstorage.set.md
new file mode 100644
index 0000000000000..7e3a2a4361244
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.sessionstorage.set.md
@@ -0,0 +1,24 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorage](./kibana-plugin-server.sessionstorage.md) > [set](./kibana-plugin-server.sessionstorage.set.md)
+
+## SessionStorage.set() method
+
+Puts current session value into the session storage.
+
+Signature:
+
+```typescript
+set(sessionValue: T): void;
+```
+
+## Parameters
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| sessionValue | T | value to put |
+
+Returns:
+
+`void`
+
diff --git a/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.asscoped.md b/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.asscoped.md
new file mode 100644
index 0000000000000..ed107ae50899b
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.asscoped.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md) > [asScoped](./kibana-plugin-server.sessionstoragefactory.asscoped.md)
+
+## SessionStorageFactory.asScoped property
+
+Signature:
+
+```typescript
+asScoped: (request: Readonly | KibanaRequest) => SessionStorage;
+```
diff --git a/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.md b/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.md
new file mode 100644
index 0000000000000..8f6f58902fde4
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.sessionstoragefactory.md
@@ -0,0 +1,20 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [SessionStorageFactory](./kibana-plugin-server.sessionstoragefactory.md)
+
+## SessionStorageFactory interface
+
+SessionStorage factory to bind one to an incoming request
+
+Signature:
+
+```typescript
+export interface SessionStorageFactory
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [asScoped](./kibana-plugin-server.sessionstoragefactory.asscoped.md) | (request: Readonly<Request> | KibanaRequest) => SessionStorage<T> | |
+
diff --git a/src/core/server/http/auth_state_storage.ts b/src/core/server/http/auth_state_storage.ts
index eafe755b79eea..bd7bf1e62968c 100644
--- a/src/core/server/http/auth_state_storage.ts
+++ b/src/core/server/http/auth_state_storage.ts
@@ -17,7 +17,7 @@
* under the License.
*/
import { Request } from 'hapi';
-import { KibanaRequest } from './router';
+import { KibanaRequest, toRawRequest } from './router';
export enum AuthStatus {
authenticated = 'authenticated',
@@ -25,17 +25,17 @@ export enum AuthStatus {
unknown = 'unknown',
}
-const toKey = (request: KibanaRequest | Request) =>
- request instanceof KibanaRequest ? request.unstable_getIncomingMessage() : request.raw.req;
+const getIncomingMessage = (request: KibanaRequest | Request) =>
+ request instanceof KibanaRequest ? toRawRequest(request).raw.req : request.raw.req;
export class AuthStateStorage {
- private readonly storage = new WeakMap, unknown>();
+ private readonly storage = new WeakMap, unknown>();
constructor(private readonly canBeAuthenticated: () => boolean) {}
public set = (request: KibanaRequest | Request, state: unknown) => {
- this.storage.set(toKey(request), state);
+ this.storage.set(getIncomingMessage(request), state);
};
public get = (request: KibanaRequest | Request) => {
- const key = toKey(request);
+ const key = getIncomingMessage(request);
const state = this.storage.get(key);
const status: AuthStatus = this.storage.has(key)
? AuthStatus.authenticated
diff --git a/src/core/server/http/cookie_session_storage.ts b/src/core/server/http/cookie_session_storage.ts
index 7301de6315606..f0cd50053cf14 100644
--- a/src/core/server/http/cookie_session_storage.ts
+++ b/src/core/server/http/cookie_session_storage.ts
@@ -19,6 +19,8 @@
import { Request, Server } from 'hapi';
import hapiAuthCookie from 'hapi-auth-cookie';
+
+import { KibanaRequest, toRawRequest } from './router';
import { SessionStorageFactory, SessionStorage } from './session_storage';
export interface SessionStorageCookieOptions {
@@ -29,10 +31,10 @@ export interface SessionStorageCookieOptions {
}
class ScopedCookieSessionStorage> implements SessionStorage {
- constructor(private readonly server: Server, private readonly request: Request) {}
+ constructor(private readonly server: Server, private readonly request: Readonly) {}
public async get(): Promise {
try {
- return await this.server.auth.test('security-cookie', this.request);
+ return await this.server.auth.test('security-cookie', this.request as Request);
} catch (error) {
return null;
}
@@ -71,8 +73,9 @@ export async function createCookieSessionStorageFactory(
});
return {
- asScoped(request: Request) {
- return new ScopedCookieSessionStorage(server, request);
+ asScoped(request: Readonly | KibanaRequest) {
+ const req = request instanceof KibanaRequest ? toRawRequest(request) : request;
+ return new ScopedCookieSessionStorage(server, req);
},
};
}
diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts
index 1e1f452556f67..9eb786ebb586a 100644
--- a/src/core/server/http/http_server.test.ts
+++ b/src/core/server/http/http_server.test.ts
@@ -18,6 +18,8 @@
*/
import { Server } from 'http';
+import request from 'request';
+import Boom from 'boom';
jest.mock('fs', () => ({
readFileSync: jest.fn(),
@@ -587,17 +589,6 @@ test('returns server and connection options on start', async () => {
expect(options).toMatchSnapshot();
});
-test('registers auth request interceptor only once', async () => {
- const { registerAuth } = await server.setup(config);
- const doRegister = () =>
- registerAuth(() => null as any, {
- encryptionKey: 'any_password',
- } as any);
-
- await doRegister();
- expect(doRegister()).rejects.toThrowError('Auth interceptor was already registered');
-});
-
test('registers registerOnPostAuth interceptor several times', async () => {
const { registerOnPostAuth } = await server.setup(config);
const doRegister = () => registerOnPostAuth(() => null as any);
@@ -698,7 +689,150 @@ const cookieOptions = {
isSecure: false,
};
-test('Should enable auth for a route by default if registerAuth has been called', async () => {
+interface User {
+ id: string;
+ roles?: string[];
+}
+
+interface StorageData {
+ value: User;
+ expires: number;
+}
+
+describe('#registerAuth', () => {
+ it('registers auth request interceptor only once', async () => {
+ const { registerAuth } = await server.setup(config);
+ const doRegister = () =>
+ registerAuth(() => null as any, {
+ encryptionKey: 'any_password',
+ } as any);
+
+ await doRegister();
+ expect(doRegister()).rejects.toThrowError('Auth interceptor was already registered');
+ });
+
+ it('supports implementing custom authentication logic', async () => {
+ const router = new Router('');
+ router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
+
+ const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
+ const { sessionStorageFactory } = await registerAuth((req, t) => {
+ const user = { id: '42' };
+ const sessionStorage = sessionStorageFactory.asScoped(req);
+ sessionStorage.set({ value: user, expires: Date.now() + 1000 });
+ return t.authenticated(user);
+ }, cookieOptions);
+ registerRouter(router);
+ await server.start();
+
+ const response = await supertest(innerServer.listener)
+ .get('/')
+ .expect(200, { content: 'ok' });
+
+ expect(response.header['set-cookie']).toBeDefined();
+ const cookies = response.header['set-cookie'];
+ expect(cookies).toHaveLength(1);
+
+ const sessionCookie = request.cookie(cookies[0]);
+ if (!sessionCookie) {
+ throw new Error('session cookie expected to be defined');
+ }
+ expect(sessionCookie).toBeDefined();
+ expect(sessionCookie.key).toBe('sid');
+ expect(sessionCookie.value).toBeDefined();
+ expect(sessionCookie.path).toBe('/');
+ expect(sessionCookie.httpOnly).toBe(true);
+ });
+
+ it('supports rejecting a request from an unauthenticated user', async () => {
+ const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
+ const router = new Router('');
+ router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
+ registerRouter(router);
+
+ await registerAuth((req, t) => t.rejected(Boom.unauthorized()), cookieOptions);
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/')
+ .expect(401);
+ });
+
+ it('supports redirecting', async () => {
+ const redirectTo = '/redirect-url';
+ const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
+ const router = new Router('');
+ router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
+ registerRouter(router);
+
+ await registerAuth((req, t) => {
+ return t.redirected(redirectTo);
+ }, cookieOptions);
+ await server.start();
+
+ const response = await supertest(innerServer.listener)
+ .get('/')
+ .expect(302);
+ expect(response.header.location).toBe(redirectTo);
+ });
+
+ it(`doesn't expose internal error details`, async () => {
+ const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
+ const router = new Router('');
+ router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
+ registerRouter(router);
+
+ await registerAuth((req, t) => {
+ throw new Error('sensitive info');
+ }, cookieOptions);
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/')
+ .expect({
+ statusCode: 500,
+ error: 'Internal Server Error',
+ message: 'An internal server error occurred',
+ });
+ });
+
+ it(`allows manipulating cookies from route handler`, async () => {
+ const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
+ const { sessionStorageFactory } = await registerAuth((req, t) => {
+ const user = { id: '42' };
+ const sessionStorage = sessionStorageFactory.asScoped(req);
+ sessionStorage.set({ value: user, expires: Date.now() + 1000 });
+ return t.authenticated();
+ }, cookieOptions);
+
+ const router = new Router('');
+ router.get({ path: '/', validate: false }, (req, res) => res.ok({ content: 'ok' }));
+ router.get({ path: '/with-cookie', validate: false }, (req, res) => {
+ const sessionStorage = sessionStorageFactory.asScoped(req);
+ sessionStorage.clear();
+ return res.ok({ content: 'ok' });
+ });
+ registerRouter(router);
+
+ await server.start();
+
+ const responseToSetCookie = await supertest(innerServer.listener)
+ .get('/')
+ .expect(200);
+
+ expect(responseToSetCookie.header['set-cookie']).toBeDefined();
+
+ const responseToResetCookie = await supertest(innerServer.listener)
+ .get('/with-cookie')
+ .expect(200);
+
+ expect(responseToResetCookie.header['set-cookie']).toEqual([
+ 'sid=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/',
+ ]);
+ });
+});
+
+test('enables auth for a route by default if registerAuth has been called', async () => {
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('');
@@ -707,9 +841,7 @@ test('Should enable auth for a route by default if registerAuth has been called'
);
registerRouter(router);
- const authenticate = jest
- .fn()
- .mockImplementation((req, sessionStorage, t) => t.authenticated({}));
+ const authenticate = jest.fn().mockImplementation((req, t) => t.authenticated());
await registerAuth(authenticate, cookieOptions);
await server.start();
@@ -720,7 +852,7 @@ test('Should enable auth for a route by default if registerAuth has been called'
expect(authenticate).toHaveBeenCalledTimes(1);
});
-test('Should support disabling auth for a route explicitly', async () => {
+test('supports disabling auth for a route explicitly', async () => {
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('');
@@ -739,7 +871,7 @@ test('Should support disabling auth for a route explicitly', async () => {
expect(authenticate).toHaveBeenCalledTimes(0);
});
-test('Should support enabling auth for a route explicitly', async () => {
+test('supports enabling auth for a route explicitly', async () => {
const { registerAuth, registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('');
@@ -747,9 +879,7 @@ test('Should support enabling auth for a route explicitly', async () => {
res.ok({ authRequired: req.route.options.authRequired })
);
registerRouter(router);
- const authenticate = jest
- .fn()
- .mockImplementation((req, sessionStorage, t) => t.authenticated({}));
+ const authenticate = jest.fn().mockImplementation((req, t) => t.authenticated({}));
await registerAuth(authenticate, cookieOptions);
await server.start();
@@ -760,7 +890,7 @@ test('Should support enabling auth for a route explicitly', async () => {
expect(authenticate).toHaveBeenCalledTimes(1);
});
-test('Should allow attaching metadata to attach meta-data tag strings to a route', async () => {
+test('allows attaching metadata to attach meta-data tag strings to a route', async () => {
const tags = ['my:tag'];
const { registerRouter, server: innerServer } = await server.setup(config);
@@ -783,7 +913,7 @@ test('Should allow attaching metadata to attach meta-data tag strings to a route
.expect(200, { tags: [] });
});
-test('Should expose route details of incoming request to a route handler', async () => {
+test('exposes route details of incoming request to a route handler', async () => {
const { registerRouter, server: innerServer } = await server.setup(config);
const router = new Router('');
@@ -813,7 +943,7 @@ describe('#auth.isAuthenticated()', () => {
);
registerRouter(router);
- await registerAuth((req, sessionStorage, t) => t.authenticated({}), cookieOptions);
+ await registerAuth((req, t) => t.authenticated(), cookieOptions);
await server.start();
await supertest(innerServer.listener)
@@ -830,7 +960,7 @@ describe('#auth.isAuthenticated()', () => {
);
registerRouter(router);
- await registerAuth((req, sessionStorage, t) => t.authenticated({}), cookieOptions);
+ await registerAuth((req, t) => t.authenticated(), cookieOptions);
await server.start();
await supertest(innerServer.listener)
@@ -855,11 +985,11 @@ describe('#auth.isAuthenticated()', () => {
});
describe('#auth.get()', () => {
- it('Should return authenticated status and allow associate auth state with request', async () => {
+ it('returns authenticated status and allow associate auth state with request', async () => {
const user = { id: '42' };
const { registerRouter, registerAuth, server: innerServer, auth } = await server.setup(config);
- await registerAuth((req, sessionStorage, t) => {
- sessionStorage.set({ value: user });
+ const { sessionStorageFactory } = await registerAuth((req, t) => {
+ sessionStorageFactory.asScoped(req).set({ value: user, expires: Date.now() + 1000 });
return t.authenticated(user);
}, cookieOptions);
@@ -873,7 +1003,7 @@ describe('#auth.get()', () => {
.expect(200, { state: user, status: 'authenticated' });
});
- it('Should return correct authentication unknown status', async () => {
+ it('returns correct authentication unknown status', async () => {
const { registerRouter, server: innerServer, auth } = await server.setup(config);
const router = new Router('');
router.get({ path: '/', validate: false }, (req, res) => res.ok(auth.get(req)));
@@ -885,7 +1015,7 @@ describe('#auth.get()', () => {
.expect(200, { status: 'unknown' });
});
- it('Should return correct unauthenticated status', async () => {
+ it('returns correct unauthenticated status', async () => {
const authenticate = jest.fn();
const { registerRouter, registerAuth, server: innerServer, auth } = await server.setup(config);
diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts
index 83eecaa2f3700..ab2d227193dce 100644
--- a/src/core/server/http/http_server.ts
+++ b/src/core/server/http/http_server.ts
@@ -26,13 +26,17 @@ import { createServer, getServerOptions } from './http_tools';
import { adoptToHapiAuthFormat, AuthenticationHandler } from './lifecycle/auth';
import { adoptToHapiOnPostAuthFormat, OnPostAuthHandler } from './lifecycle/on_post_auth';
import { adoptToHapiOnPreAuthFormat, OnPreAuthHandler } from './lifecycle/on_pre_auth';
-import { Router, KibanaRequest } from './router';
+import { Router, KibanaRequest, toRawRequest } from './router';
import {
SessionStorageCookieOptions,
createCookieSessionStorageFactory,
} from './cookie_session_storage';
+import { SessionStorageFactory } from './session_storage';
import { AuthStateStorage } from './auth_state_storage';
+const getIncomingMessage = (request: KibanaRequest | Request) =>
+ request instanceof KibanaRequest ? toRawRequest(request).raw.req : request.raw.req;
+
export interface HttpServerSetup {
server: Server;
options: ServerOptions;
@@ -44,9 +48,9 @@ export interface HttpServerSetup {
* Only one AuthenticationHandler can be registered.
*/
registerAuth: (
- handler: AuthenticationHandler,
+ handler: AuthenticationHandler,
cookieOptions: SessionStorageCookieOptions
- ) => Promise;
+ ) => Promise<{ sessionStorageFactory: SessionStorageFactory }>;
/**
* To define custom logic to perform for incoming requests. Runs the handler before Auth
* hook performs a check that user has access to requested resources, so it's the only
@@ -76,10 +80,7 @@ export class HttpServer {
private config?: HttpConfig;
private registeredRouters = new Set();
private authRegistered = false;
- private basePathCache = new WeakMap<
- ReturnType,
- string
- >();
+ private basePathCache = new WeakMap, string>();
private readonly authState: AuthStateStorage;
@@ -102,8 +103,7 @@ export class HttpServer {
// passing hapi Request works for BWC. can be deleted once we remove legacy server.
private getBasePathFor(config: HttpConfig, request: KibanaRequest | Request) {
- const incomingMessage =
- request instanceof KibanaRequest ? request.unstable_getIncomingMessage() : request.raw.req;
+ const incomingMessage = getIncomingMessage(request);
const requestScopePath = this.basePathCache.get(incomingMessage) || '';
const serverBasePath = config.basePath || '';
@@ -112,8 +112,8 @@ export class HttpServer {
// should work only for KibanaRequest as soon as spaces migrate to NP
private setBasePathFor(request: KibanaRequest | Request, basePath: string) {
- const incomingMessage =
- request instanceof KibanaRequest ? request.unstable_getIncomingMessage() : request.raw.req;
+ const incomingMessage = getIncomingMessage(request);
+
if (this.basePathCache.has(incomingMessage)) {
throw new Error(
'Request basePath was previously set. Setting multiple times is not supported.'
@@ -134,10 +134,8 @@ export class HttpServer {
registerRouter: this.registerRouter.bind(this),
registerOnPreAuth: this.registerOnPreAuth.bind(this),
registerOnPostAuth: this.registerOnPostAuth.bind(this),
- registerAuth: (
- fn: AuthenticationHandler,
- cookieOptions: SessionStorageCookieOptions
- ) => this.registerAuth(fn, cookieOptions, config.basePath),
+ registerAuth: (fn: AuthenticationHandler, cookieOptions: SessionStorageCookieOptions) =>
+ this.registerAuth(fn, cookieOptions, config.basePath),
getBasePathFor: this.getBasePathFor.bind(this, config),
setBasePathFor: this.setBasePathFor.bind(this),
auth: {
@@ -234,7 +232,7 @@ export class HttpServer {
}
private async registerAuth(
- fn: AuthenticationHandler,
+ fn: AuthenticationHandler,
cookieOptions: SessionStorageCookieOptions,
basePath?: string
) {
@@ -246,14 +244,14 @@ export class HttpServer {
}
this.authRegistered = true;
- const sessionStorage = await createCookieSessionStorageFactory(
+ const sessionStorageFactory = await createCookieSessionStorageFactory(
this.server,
cookieOptions,
basePath
);
this.server.auth.scheme('login', () => ({
- authenticate: adoptToHapiAuthFormat(fn, sessionStorage, this.authState.set),
+ authenticate: adoptToHapiAuthFormat(fn, this.authState.set),
}));
this.server.auth.strategy('session', 'login');
@@ -262,5 +260,7 @@ export class HttpServer {
// should be applied for all routes if they don't specify auth strategy in route declaration
// https://github.com/hapijs/hapi/blob/master/API.md#-serverauthdefaultoptions
this.server.auth.default('session');
+
+ return { sessionStorageFactory };
}
}
diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts
index f1ab7c629689b..056ee53cee89b 100644
--- a/src/core/server/http/index.ts
+++ b/src/core/server/http/index.ts
@@ -30,3 +30,4 @@ export { BasePathProxyServer } from './base_path_proxy_server';
export { OnPreAuthHandler, OnPreAuthToolkit } from './lifecycle/on_pre_auth';
export { AuthenticationHandler, AuthToolkit } from './lifecycle/auth';
export { OnPostAuthHandler, OnPostAuthToolkit } from './lifecycle/on_post_auth';
+export { SessionStorageFactory, SessionStorage } from './session_storage';
diff --git a/src/core/server/http/integration_tests/http_service.test.ts b/src/core/server/http/integration_tests/http_service.test.ts
index 93fe20a80e120..100efc4e2a607 100644
--- a/src/core/server/http/integration_tests/http_service.test.ts
+++ b/src/core/server/http/integration_tests/http_service.test.ts
@@ -16,12 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
-import request from 'request';
import Boom from 'boom';
-import { AuthenticationHandler } from '../../../../core/server';
import { Router } from '../router';
-
import * as kbnTestServer from '../../../../test_utils/kbn_server';
interface User {
@@ -29,7 +26,7 @@ interface User {
roles?: string[];
}
-interface Storage {
+interface StorageData {
value: User;
expires: number;
}
@@ -41,7 +38,7 @@ describe('http service', () => {
const cookieOptions = {
name: 'sid',
encryptionKey: 'something_at_least_32_characters',
- validate: (session: Storage) => true,
+ validate: (session: StorageData) => true,
isSecure: false,
path: '/',
};
@@ -53,90 +50,18 @@ describe('http service', () => {
afterEach(async () => await root.shutdown());
- it('Should support implementing custom authentication logic', async () => {
- const router = new Router('');
- router.get({ path: '/', validate: false }, async (req, res) => res.ok({ content: 'ok' }));
-
- const authenticate: AuthenticationHandler = async (req, sessionStorage, t) => {
- if (req.headers.authorization) {
- const user = { id: '42' };
- sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs });
- return t.authenticated(user);
- } else {
- return t.rejected(Boom.unauthorized());
- }
- };
-
- const { http } = await root.setup();
- await http.registerAuth(authenticate, cookieOptions);
- http.registerRouter(router);
- await root.start();
-
- const response = await kbnTestServer.request.get(root, '/').expect(200, { content: 'ok' });
-
- expect(response.header['set-cookie']).toBeDefined();
- const cookies = response.header['set-cookie'];
- expect(cookies).toHaveLength(1);
-
- const sessionCookie = request.cookie(cookies[0]);
- if (!sessionCookie) {
- throw new Error('session cookie expected to be defined');
- }
- expect(sessionCookie).toBeDefined();
- expect(sessionCookie.key).toBe('sid');
- expect(sessionCookie.value).toBeDefined();
- expect(sessionCookie.path).toBe('/');
- expect(sessionCookie.httpOnly).toBe(true);
- });
-
- it('Should support rejecting a request from an unauthenticated user', async () => {
- const authenticate: AuthenticationHandler = async (req, sessionStorage, t) => {
- if (req.headers.authorization) {
- const user = { id: '42' };
- sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs });
- return t.authenticated(user);
- } else {
- return t.rejected(Boom.unauthorized());
- }
- };
-
- const { http } = await root.setup();
- await http.registerAuth(authenticate, cookieOptions);
- await root.start();
-
- await kbnTestServer.request
- .get(root, '/')
- .unset('Authorization')
- .expect(401);
- });
-
- it('Should support redirecting', async () => {
- const redirectTo = '/redirect-url';
- const authenticate: AuthenticationHandler = async (req, sessionStorage, t) => {
- return t.redirected(redirectTo);
- };
-
- const { http } = await root.setup();
- await http.registerAuth(authenticate, cookieOptions);
- await root.start();
-
- const response = await kbnTestServer.request.get(root, '/').expect(302);
- expect(response.header.location).toBe(redirectTo);
- });
-
it('Should run auth for legacy routes and proxy request to legacy server route handlers', async () => {
- const authenticate: AuthenticationHandler = async (req, sessionStorage, t) => {
+ const { http } = await root.setup();
+ const { sessionStorageFactory } = await http.registerAuth((req, t) => {
if (req.headers.authorization) {
const user = { id: '42' };
+ const sessionStorage = sessionStorageFactory.asScoped(req);
sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs });
return t.authenticated(user);
} else {
return t.rejected(Boom.unauthorized());
}
- };
-
- const { http } = await root.setup();
- await http.registerAuth(authenticate, cookieOptions);
+ }, cookieOptions);
await root.start();
const legacyUrl = '/legacy';
@@ -156,17 +81,17 @@ describe('http service', () => {
it('Should pass associated auth state to Legacy platform', async () => {
const user = { id: '42' };
- const authenticate: AuthenticationHandler = async (req, sessionStorage, t) => {
+
+ const { http } = await root.setup();
+ const { sessionStorageFactory } = await http.registerAuth((req, t) => {
if (req.headers.authorization) {
+ const sessionStorage = sessionStorageFactory.asScoped(req);
sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs });
return t.authenticated(user);
} else {
return t.rejected(Boom.unauthorized());
}
- };
-
- const { http } = await root.setup();
- await http.registerAuth(authenticate, cookieOptions);
+ }, cookieOptions);
await root.start();
const legacyUrl = '/legacy';
@@ -183,22 +108,6 @@ describe('http service', () => {
expect(response.header['set-cookie']).toBe(undefined);
});
-
- it(`Shouldn't expose internal error details`, async () => {
- const authenticate: AuthenticationHandler = async (req, sessionStorage, t) => {
- throw new Error('sensitive info');
- };
-
- const { http } = await root.setup();
- await http.registerAuth(authenticate, cookieOptions);
- await root.start();
-
- await kbnTestServer.request.get(root, '/').expect({
- statusCode: 500,
- error: 'Internal Server Error',
- message: 'An internal server error occurred',
- });
- });
});
describe('#registerOnPostAuth()', () => {
diff --git a/src/core/server/http/lifecycle/auth.test.ts b/src/core/server/http/lifecycle/auth.test.ts
index f67650d490b03..031556c70483c 100644
--- a/src/core/server/http/lifecycle/auth.test.ts
+++ b/src/core/server/http/lifecycle/auth.test.ts
@@ -21,18 +21,11 @@ import Boom from 'boom';
import { adoptToHapiAuthFormat } from './auth';
import { httpServerMock } from '../http_server.mocks';
-const SessionStorageMock = {
- asScoped: () => null as any,
-};
-
describe('adoptToHapiAuthFormat', () => {
it('Should allow authenticating a user identity with given credentials', async () => {
const credentials = {};
const authenticatedMock = jest.fn();
- const onAuth = adoptToHapiAuthFormat(
- async (req, sessionStorage, t) => t.authenticated(credentials),
- SessionStorageMock
- );
+ const onAuth = adoptToHapiAuthFormat((req, t) => t.authenticated(credentials));
await onAuth(
httpServerMock.createRawRequest(),
httpServerMock.createRawResponseToolkit({
@@ -46,10 +39,7 @@ describe('adoptToHapiAuthFormat', () => {
it('Should allow redirecting to specified url', async () => {
const redirectUrl = '/docs';
- const onAuth = adoptToHapiAuthFormat(
- async (req, sessionStorage, t) => t.redirected(redirectUrl),
- SessionStorageMock
- );
+ const onAuth = adoptToHapiAuthFormat((req, t) => t.redirected(redirectUrl));
const takeoverSymbol = {};
const redirectMock = jest.fn(() => ({ takeover: () => takeoverSymbol }));
const result = await onAuth(
@@ -64,9 +54,8 @@ describe('adoptToHapiAuthFormat', () => {
});
it('Should allow to specify statusCode and message for Boom error', async () => {
- const onAuth = adoptToHapiAuthFormat(
- async (req, sessionStorage, t) => t.rejected(new Error('not found'), { statusCode: 404 }),
- SessionStorageMock
+ const onAuth = adoptToHapiAuthFormat((req, t) =>
+ t.rejected(new Error('not found'), { statusCode: 404 })
);
const result = (await onAuth(
httpServerMock.createRawRequest(),
@@ -79,9 +68,9 @@ describe('adoptToHapiAuthFormat', () => {
});
it('Should return Boom.internal error error if interceptor throws', async () => {
- const onAuth = adoptToHapiAuthFormat(async (req, sessionStorage, t) => {
+ const onAuth = adoptToHapiAuthFormat((req, t) => {
throw new Error('unknown error');
- }, SessionStorageMock);
+ });
const result = (await onAuth(
httpServerMock.createRawRequest(),
httpServerMock.createRawResponseToolkit()
@@ -93,10 +82,7 @@ describe('adoptToHapiAuthFormat', () => {
});
it('Should return Boom.internal error if interceptor returns unexpected result', async () => {
- const onAuth = adoptToHapiAuthFormat(
- async (req, sessionStorage, t) => undefined as any,
- SessionStorageMock
- );
+ const onAuth = adoptToHapiAuthFormat(async (req, t) => undefined as any);
const result = (await onAuth(
httpServerMock.createRawRequest(),
httpServerMock.createRawResponseToolkit()
diff --git a/src/core/server/http/lifecycle/auth.ts b/src/core/server/http/lifecycle/auth.ts
index ffe77e0120fe4..bcb7e454b4119 100644
--- a/src/core/server/http/lifecycle/auth.ts
+++ b/src/core/server/http/lifecycle/auth.ts
@@ -19,7 +19,6 @@
import Boom from 'boom';
import { noop } from 'lodash';
import { Lifecycle, Request, ResponseToolkit } from 'hapi';
-import { SessionStorage, SessionStorageFactory } from '../session_storage';
enum ResultType {
authenticated = 'authenticated',
@@ -46,7 +45,7 @@ interface Rejected {
type AuthResult = Authenticated | Rejected | Redirected;
const authResult = {
- authenticated(state: object): AuthResult {
+ authenticated(state: object = {}): AuthResult {
return { type: ResultType.authenticated, state };
},
redirected(url: string): AuthResult {
@@ -80,7 +79,7 @@ const authResult = {
*/
export interface AuthToolkit {
/** Authentication is successful with given credentials, allow request to pass through */
- authenticated: (state: object) => AuthResult;
+ authenticated: (state?: object) => AuthResult;
/** Authentication requires to interrupt request handling and redirect to a configured url */
redirected: (url: string) => AuthResult;
/** Authentication is unsuccessful, fail the request with specified error. */
@@ -94,16 +93,14 @@ const toolkit: AuthToolkit = {
};
/** @public */
-export type AuthenticationHandler = (
+export type AuthenticationHandler = (
request: Readonly,
- sessionStorage: SessionStorage,
t: AuthToolkit
) => AuthResult | Promise;
/** @public */
-export function adoptToHapiAuthFormat(
- fn: AuthenticationHandler,
- sessionStorage: SessionStorageFactory,
+export function adoptToHapiAuthFormat(
+ fn: AuthenticationHandler,
onSuccess: (req: Request, state: unknown) => void = noop
) {
return async function interceptAuth(
@@ -111,7 +108,7 @@ export function adoptToHapiAuthFormat(
h: ResponseToolkit
): Promise {
try {
- const result = await fn(req, sessionStorage.asScoped(req), toolkit);
+ const result = await fn(req, toolkit);
if (!authResult.isValid(result)) {
throw new Error(
`Unexpected result from Authenticate. Expected AuthResult, but given: ${result}.`
diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts
index 9fc9d530ccaa2..cb941326e23f1 100644
--- a/src/core/server/http/router/index.ts
+++ b/src/core/server/http/router/index.ts
@@ -19,5 +19,5 @@
export { Headers, filterHeaders } from './headers';
export { Router } from './router';
-export { KibanaRequest, KibanaRequestRoute } from './request';
+export { KibanaRequest, KibanaRequestRoute, toRawRequest } from './request';
export { RouteMethod, RouteConfigOptions } from './route';
diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts
index 82db991f8e392..3c235ffbf8bd9 100644
--- a/src/core/server/http/router/request.ts
+++ b/src/core/server/http/router/request.ts
@@ -25,6 +25,8 @@ import { deepFreeze, RecursiveReadonly } from '../../../utils';
import { filterHeaders, Headers } from './headers';
import { RouteMethod, RouteSchemas, RouteConfigOptions } from './route';
+const requestSymbol = Symbol('request');
+
/**
* Request specific route information exposed to a handler.
* @public
@@ -43,6 +45,7 @@ export class KibanaRequest {
/**
* Factory for creating requests. Validates the request before creating an
* instance of a KibanaRequest.
+ * @internal
*/
public static from(
req: Request,
@@ -87,14 +90,19 @@ export class KibanaRequest {
public readonly url: Url;
public readonly route: RecursiveReadonly;
+ /** @internal */
+ protected readonly [requestSymbol]: Request;
+
constructor(
- private readonly request: Request,
+ request: Request,
readonly params: Params,
readonly query: Query,
readonly body: Body
) {
this.headers = request.headers;
this.url = request.url;
+
+ this[requestSymbol] = request;
this.route = deepFreeze(this.getRouteInfo());
}
@@ -102,19 +110,21 @@ export class KibanaRequest {
return filterHeaders(this.headers, headersToKeep);
}
- // eslint-disable-next-line @typescript-eslint/camelcase
- public unstable_getIncomingMessage() {
- return this.request.raw.req;
- }
-
private getRouteInfo() {
+ const request = this[requestSymbol];
return {
- path: this.request.path,
- method: this.request.method,
+ path: request.path,
+ method: request.method,
options: {
- authRequired: this.request.route.settings.auth !== false,
- tags: this.request.route.settings.tags || [],
+ authRequired: request.route.settings.auth !== false,
+ tags: request.route.settings.tags || [],
},
};
}
}
+
+/**
+ * Returns underlying Hapi Request object for KibanaRequest
+ * @internal
+ */
+export const toRawRequest = (request: KibanaRequest) => request[requestSymbol];
diff --git a/src/core/server/http/session_storage.ts b/src/core/server/http/session_storage.ts
index 4f9d28991fe78..2c726ce34a3cb 100644
--- a/src/core/server/http/session_storage.ts
+++ b/src/core/server/http/session_storage.ts
@@ -18,8 +18,10 @@
*/
import { Request } from 'hapi';
+import { KibanaRequest } from './router';
/**
* Provides an interface to store and retrieve data across requests.
+ * @public
*/
export interface SessionStorage {
/**
@@ -37,6 +39,9 @@ export interface SessionStorage {
clear(): void;
}
+/**
+ * SessionStorage factory to bind one to an incoming request
+ * @public */
export interface SessionStorageFactory {
- asScoped: (request: Request) => SessionStorage;
+ asScoped: (request: Readonly | KibanaRequest) => SessionStorage;
}
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index a6cb044ca2c54..4cebe24f39f15 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -62,6 +62,8 @@ export {
Router,
RouteMethod,
RouteConfigOptions,
+ SessionStorageFactory,
+ SessionStorage,
} from './http';
export { Logger, LoggerFactory, LogMeta, LogRecord, LogLevel } from './logging';
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 442e7b79440e4..88ebe1e54bdce 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -22,15 +22,14 @@ import { Url } from 'url';
// @public (undocumented)
export type APICaller = (endpoint: string, clientParams: Record, options?: CallAPIOptions) => Promise;
-// Warning: (ae-forgotten-export) The symbol "SessionStorage" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "AuthResult" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
-export type AuthenticationHandler = (request: Readonly, sessionStorage: SessionStorage, t: AuthToolkit) => AuthResult | Promise;
+export type AuthenticationHandler = (request: Readonly, t: AuthToolkit) => AuthResult | Promise;
// @public
export interface AuthToolkit {
- authenticated: (state: object) => AuthResult;
+ authenticated: (state?: object) => AuthResult;
redirected: (url: string) => AuthResult;
rejected: (error: Error, options?: {
statusCode?: number;
@@ -167,10 +166,14 @@ export interface InternalCoreStart {
// @public
export class KibanaRequest {
+ // @internal (undocumented)
+ protected readonly [requestSymbol]: Request;
constructor(request: Request, params: Params, query: Query, body: Body);
// (undocumented)
readonly body: Body;
// Warning: (ae-forgotten-export) The symbol "RouteSchemas" needs to be exported by the entry point index.d.ts
+ //
+ // @internal
static from(req: Request, routeSchemas?: RouteSchemas
): KibanaRequest
;
// (undocumented)
getFilteredHeaders(headersToKeep: string[]): Pick, string>;
@@ -183,8 +186,6 @@ export class KibanaRequest {
// (undocumented)
readonly route: RecursiveReadonly;
// (undocumented)
- unstable_getIncomingMessage(): import("http").IncomingMessage;
- // (undocumented)
readonly url: Url;
}
@@ -386,6 +387,19 @@ export class ScopedClusterClient {
callAsInternalUser(endpoint: string, clientParams?: Record, options?: CallAPIOptions): Promise;
}
+// @public
+export interface SessionStorage {
+ clear(): void;
+ get(): Promise;
+ set(sessionValue: T): void;
+}
+
+// @public
+export interface SessionStorageFactory {
+ // (undocumented)
+ asScoped: (request: Readonly | KibanaRequest) => SessionStorage;
+}
+
// Warnings were encountered during analysis:
//