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'];