diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.http.md b/docs/development/core/server/kibana-plugin-server.coresetup.http.md
index 1ab58b6c3d99..c9206b7a7e71 100644
--- a/docs/development/core/server/kibana-plugin-server.coresetup.http.md
+++ b/docs/development/core/server/kibana-plugin-server.coresetup.http.md
@@ -8,6 +8,7 @@
```typescript
http: {
+ createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.md b/docs/development/core/server/kibana-plugin-server.coresetup.md
index bc13bad563ac..f4653d7f4357 100644
--- a/docs/development/core/server/kibana-plugin-server.coresetup.md
+++ b/docs/development/core/server/kibana-plugin-server.coresetup.md
@@ -17,5 +17,5 @@ export interface CoreSetup
| Property | Type | Description |
| --- | --- | --- |
| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | {
adminClient$: Observable<ClusterClient>;
dataClient$: Observable<ClusterClient>;
createClient: (type: string, clientConfig?: Partial<ElasticsearchClientConfig>) => ClusterClient;
}
| |
-| [http](./kibana-plugin-server.coresetup.http.md) | {
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
basePath: HttpServiceSetup['basePath'];
createNewServer: HttpServiceSetup['createNewServer'];
isTlsEnabled: HttpServiceSetup['isTlsEnabled'];
}
| |
+| [http](./kibana-plugin-server.coresetup.http.md) | {
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
basePath: HttpServiceSetup['basePath'];
createNewServer: HttpServiceSetup['createNewServer'];
isTlsEnabled: HttpServiceSetup['isTlsEnabled'];
}
| |
diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts
index c8a987352883..aa341db20a6c 100644
--- a/src/core/server/http/http_server.test.ts
+++ b/src/core/server/http/http_server.test.ts
@@ -603,7 +603,7 @@ test('enables auth for a route by default if registerAuth has been called', asyn
registerRouter(router);
const authenticate = jest.fn().mockImplementation((req, t) => t.authenticated());
- await registerAuth(authenticate, cookieOptions);
+ registerAuth(authenticate);
await server.start();
await supertest(innerServer.listener)
@@ -622,7 +622,7 @@ test('supports disabling auth for a route explicitly', async () => {
);
registerRouter(router);
const authenticate = jest.fn();
- await registerAuth(authenticate, cookieOptions);
+ registerAuth(authenticate);
await server.start();
await supertest(innerServer.listener)
@@ -641,7 +641,7 @@ test('supports enabling auth for a route explicitly', async () => {
);
registerRouter(router);
const authenticate = jest.fn().mockImplementation((req, t) => t.authenticated({}));
- await registerAuth(authenticate, cookieOptions);
+ await registerAuth(authenticate);
await server.start();
await supertest(innerServer.listener)
@@ -695,29 +695,115 @@ test('exposes route details of incoming request to a route handler', async () =>
});
describe('setup contract', () => {
+ describe('#createSessionStorage', () => {
+ it('creates session storage factory', async () => {
+ const { createCookieSessionStorageFactory } = await server.setup(config);
+ const sessionStorageFactory = await createCookieSessionStorageFactory(cookieOptions);
+
+ expect(sessionStorageFactory.asScoped).toBeDefined();
+ });
+ it('creates session storage factory only once', async () => {
+ const { createCookieSessionStorageFactory } = await server.setup(config);
+ const create = async () => await createCookieSessionStorageFactory(cookieOptions);
+
+ await create();
+ expect(create()).rejects.toThrowError('A cookieSessionStorageFactory was already created');
+ });
+ });
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);
+ const doRegister = () => registerAuth(() => null as any);
- await doRegister();
- expect(doRegister()).rejects.toThrowError('Auth interceptor was already registered');
+ doRegister();
+ expect(doRegister).toThrowError('Auth interceptor was already registered');
});
- it('supports implementing custom authentication logic', async () => {
+ it('may grant access to a resource', 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.authenticated());
+ await server.start();
+
+ await supertest(innerServer.listener)
+ .get('/')
+ .expect(200, { content: 'ok' });
+ });
+
+ 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()));
+ 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);
+ });
+ 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');
+ });
+ 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 via cookie session storage', 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 {
+ createCookieSessionStorageFactory,
+ registerAuth,
+ registerRouter,
+ server: innerServer,
+ } = await server.setup(config);
+ const sessionStorageFactory = await createCookieSessionStorageFactory(
+ cookieOptions
+ );
+ registerAuth((req, t) => {
const user = { id: '42' };
const sessionStorage = sessionStorageFactory.asScoped(req);
sessionStorage.set({ value: user, expires: Date.now() + 1000 });
return t.authenticated({ state: user });
- }, cookieOptions);
+ });
registerRouter(router);
await server.start();
@@ -740,66 +826,22 @@ describe('setup contract', () => {
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 {
+ createCookieSessionStorageFactory,
+ registerAuth,
+ registerRouter,
+ server: innerServer,
+ } = await server.setup(config);
+ const sessionStorageFactory = await createCookieSessionStorageFactory(
+ cookieOptions
+ );
+ 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' }));
@@ -847,7 +889,7 @@ describe('setup contract', () => {
await registerAuth((req, t) => {
fromRegisterAuth = req.headers.authorization;
return t.authenticated();
- }, cookieOptions);
+ });
let fromRegisterOnPostAuth;
await registerOnPostAuth((req, t) => {
@@ -889,7 +931,7 @@ describe('setup contract', () => {
);
registerRouter(router);
- await registerAuth((req, t) => t.authenticated(), cookieOptions);
+ await registerAuth((req, t) => t.authenticated());
await server.start();
await supertest(innerServer.listener)
@@ -908,7 +950,7 @@ describe('setup contract', () => {
);
registerRouter(router);
- await registerAuth((req, t) => t.authenticated(), cookieOptions);
+ await registerAuth((req, t) => t.authenticated());
await server.start();
await supertest(innerServer.listener)
@@ -935,13 +977,18 @@ describe('setup contract', () => {
describe('#auth.get()', () => {
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
- );
- const { sessionStorageFactory } = await registerAuth((req, t) => {
+ const {
+ createCookieSessionStorageFactory,
+ registerRouter,
+ registerAuth,
+ server: innerServer,
+ auth,
+ } = await server.setup(config);
+ const sessionStorageFactory = await createCookieSessionStorageFactory(cookieOptions);
+ registerAuth((req, t) => {
sessionStorageFactory.asScoped(req).set({ value: user, expires: Date.now() + 1000 });
return t.authenticated({ state: user });
- }, cookieOptions);
+ });
const router = new Router('');
router.get({ path: '/', validate: false }, (req, res) => res.ok(auth.get(req)));
@@ -971,7 +1018,7 @@ describe('setup contract', () => {
const { registerRouter, registerAuth, server: innerServer, auth } = await server.setup(
config
);
- await registerAuth(authenticate, cookieOptions);
+ await registerAuth(authenticate);
const router = new Router('');
router.get({ path: '/', validate: false, options: { authRequired: false } }, (req, res) =>
res.ok(auth.get(req))
diff --git a/src/core/server/http/http_server.ts b/src/core/server/http/http_server.ts
index 4a5b88bd105a..4ca76d405a1f 100644
--- a/src/core/server/http/http_server.ts
+++ b/src/core/server/http/http_server.ts
@@ -38,16 +38,19 @@ import { BasePath } from './base_path_service';
export interface HttpServerSetup {
server: Server;
registerRouter: (router: Router) => void;
+ /**
+ * Creates cookie based session storage factory {@link SessionStorageFactory}
+ */
+ createCookieSessionStorageFactory: (
+ cookieOptions: SessionStorageCookieOptions
+ ) => Promise>;
/**
* To define custom authentication and/or authorization mechanism for incoming requests.
* A handler should return a state to associate with the incoming request.
* The state can be retrieved later via http.auth.get(..)
* Only one AuthenticationHandler can be registered.
*/
- registerAuth: (
- handler: AuthenticationHandler,
- cookieOptions: SessionStorageCookieOptions
- ) => Promise<{ sessionStorageFactory: SessionStorageFactory }>;
+ registerAuth: (handler: AuthenticationHandler) => void;
/**
* 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
@@ -83,6 +86,7 @@ export class HttpServer {
private config?: HttpConfig;
private registeredRouters = new Set();
private authRegistered = false;
+ private cookieSessionStorageCreated = false;
private readonly log: Logger;
private readonly authState: AuthStateStorage;
@@ -120,8 +124,9 @@ 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),
+ createCookieSessionStorageFactory: (cookieOptions: SessionStorageCookieOptions) =>
+ this.createCookieSessionStorageFactory(cookieOptions, config.basePath),
+ registerAuth: this.registerAuth.bind(this),
basePath: basePathService,
auth: {
get: this.authState.get,
@@ -212,11 +217,27 @@ export class HttpServer {
this.server.ext('onRequest', adoptToHapiOnPreAuthFormat(fn));
}
- private async registerAuth(
- fn: AuthenticationHandler,
+ private async createCookieSessionStorageFactory(
cookieOptions: SessionStorageCookieOptions,
basePath?: string
) {
+ if (this.server === undefined) {
+ throw new Error('Server is not created yet');
+ }
+ if (this.cookieSessionStorageCreated) {
+ throw new Error('A cookieSessionStorageFactory was already created');
+ }
+ this.cookieSessionStorageCreated = true;
+ const sessionStorageFactory = await createCookieSessionStorageFactory(
+ this.logger.get('http', 'server', this.name, 'cookie-session-storage'),
+ this.server,
+ cookieOptions,
+ basePath
+ );
+ return sessionStorageFactory;
+ }
+
+ private registerAuth(fn: AuthenticationHandler) {
if (this.server === undefined) {
throw new Error('Server is not created yet');
}
@@ -225,13 +246,6 @@ export class HttpServer {
}
this.authRegistered = true;
- const sessionStorageFactory = await createCookieSessionStorageFactory(
- this.logger.get('http', 'server', this.name, 'cookie-session-storage'),
- this.server,
- cookieOptions,
- basePath
- );
-
this.server.auth.scheme('login', () => ({
authenticate: adoptToHapiAuthFormat(fn, (req, { state, headers }) => {
this.authState.set(req, state);
@@ -248,7 +262,5 @@ 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/http_service.mock.ts b/src/core/server/http/http_service.mock.ts
index b3afc8d84f6b..02103fc4acc8 100644
--- a/src/core/server/http/http_service.mock.ts
+++ b/src/core/server/http/http_service.mock.ts
@@ -41,6 +41,7 @@ const createSetupContractMock = () => {
const setupContract: ServiceSetupMockType = {
// we can mock some hapi server method when we need it
server: {} as Server,
+ createCookieSessionStorageFactory: jest.fn(),
registerOnPreAuth: jest.fn(),
registerAuth: jest.fn(),
registerOnPostAuth: jest.fn(),
@@ -55,9 +56,9 @@ const createSetupContractMock = () => {
isTlsEnabled: false,
};
setupContract.createNewServer.mockResolvedValue({} as HttpServerSetup);
- setupContract.registerAuth.mockResolvedValue({
- sessionStorageFactory: sessionStorageMock.createFactory(),
- });
+ setupContract.createCookieSessionStorageFactory.mockResolvedValue(
+ sessionStorageMock.createFactory()
+ );
return setupContract;
};
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 b35d20a58ff5..3c3ee866ff55 100644
--- a/src/core/server/http/integration_tests/http_service.test.ts
+++ b/src/core/server/http/integration_tests/http_service.test.ts
@@ -58,7 +58,10 @@ describe('http service', () => {
it('runs auth for legacy routes and proxy request to legacy server route handlers', async () => {
const { http } = await root.setup();
- const { sessionStorageFactory } = await http.registerAuth((req, t) => {
+ const sessionStorageFactory = await http.createCookieSessionStorageFactory(
+ cookieOptions
+ );
+ http.registerAuth((req, t) => {
if (req.headers.authorization) {
const user = { id: '42' };
const sessionStorage = sessionStorageFactory.asScoped(req);
@@ -67,7 +70,7 @@ describe('http service', () => {
} else {
return t.rejected(Boom.unauthorized());
}
- }, cookieOptions);
+ });
await root.start();
const legacyUrl = '/legacy';
@@ -88,7 +91,10 @@ describe('http service', () => {
it('passes authHeaders as request headers to the legacy platform', async () => {
const token = 'Basic: name:password';
const { http } = await root.setup();
- const { sessionStorageFactory } = await http.registerAuth((req, t) => {
+ const sessionStorageFactory = await http.createCookieSessionStorageFactory(
+ cookieOptions
+ );
+ http.registerAuth((req, t) => {
if (req.headers.authorization) {
const user = { id: '42' };
const sessionStorage = sessionStorageFactory.asScoped(req);
@@ -102,7 +108,7 @@ describe('http service', () => {
} else {
return t.rejected(Boom.unauthorized());
}
- }, cookieOptions);
+ });
await root.start();
const legacyUrl = '/legacy';
@@ -126,7 +132,10 @@ describe('http service', () => {
const user = { id: '42' };
const { http } = await root.setup();
- const { sessionStorageFactory } = await http.registerAuth((req, t) => {
+ const sessionStorageFactory = await http.createCookieSessionStorageFactory(
+ cookieOptions
+ );
+ http.registerAuth((req, t) => {
if (req.headers.authorization) {
const sessionStorage = sessionStorageFactory.asScoped(req);
sessionStorage.set({ value: user, expires: Date.now() + sessionDurationMs });
@@ -134,7 +143,7 @@ describe('http service', () => {
} else {
return t.rejected(Boom.unauthorized());
}
- }, cookieOptions);
+ });
await root.start();
const legacyUrl = '/legacy';
@@ -159,7 +168,7 @@ describe('http service', () => {
await registerAuth((req, t) => {
return t.authenticated({ headers: authHeaders });
- }, cookieOptions);
+ });
const router = new Router('/new-platform');
router.get({ path: '/', validate: false }, async (req, res) => {
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index 787478d5b3c3..4582f1362922 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -125,6 +125,7 @@ export interface CoreSetup {
) => ClusterClient;
};
http: {
+ createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];
diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts
index 88039238af09..fcc8a26f51b4 100644
--- a/src/core/server/plugins/plugin_context.ts
+++ b/src/core/server/plugins/plugin_context.ts
@@ -118,6 +118,7 @@ export function createPluginSetupContext(
createClient: deps.elasticsearch.createClient,
},
http: {
+ createCookieSessionStorageFactory: deps.http.createCookieSessionStorageFactory,
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 77059cd1491a..a6fbfebf9d94 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -93,6 +93,7 @@ export interface CoreSetup {
};
// (undocumented)
http: {
+ createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];