decouple sessiontStorageFactory creation from registerAuth (#40852)

* decouple sessiontStorageFactory creation from registerAuth

* expose to plugins

* re-generate docs

* fix mocks
This commit is contained in:
Mikhail Shustov 2019-07-16 16:39:14 +02:00 committed by GitHub
parent 5d7e2c2544
commit 51374d6a91
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 178 additions and 105 deletions

View file

@ -8,6 +8,7 @@
```typescript
http: {
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];

View file

@ -17,5 +17,5 @@ export interface CoreSetup
| Property | Type | Description |
| --- | --- | --- |
| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | <code>{</code><br/><code> adminClient$: Observable&lt;ClusterClient&gt;;</code><br/><code> dataClient$: Observable&lt;ClusterClient&gt;;</code><br/><code> createClient: (type: string, clientConfig?: Partial&lt;ElasticsearchClientConfig&gt;) =&gt; ClusterClient;</code><br/><code> }</code> | |
| [http](./kibana-plugin-server.coresetup.http.md) | <code>{</code><br/><code> registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];</code><br/><code> registerAuth: HttpServiceSetup['registerAuth'];</code><br/><code> registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];</code><br/><code> basePath: HttpServiceSetup['basePath'];</code><br/><code> createNewServer: HttpServiceSetup['createNewServer'];</code><br/><code> isTlsEnabled: HttpServiceSetup['isTlsEnabled'];</code><br/><code> }</code> | |
| [http](./kibana-plugin-server.coresetup.http.md) | <code>{</code><br/><code> createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];</code><br/><code> registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];</code><br/><code> registerAuth: HttpServiceSetup['registerAuth'];</code><br/><code> registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];</code><br/><code> basePath: HttpServiceSetup['basePath'];</code><br/><code> createNewServer: HttpServiceSetup['createNewServer'];</code><br/><code> isTlsEnabled: HttpServiceSetup['isTlsEnabled'];</code><br/><code> }</code> | |

View file

@ -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<StorageData>((req, t) => {
const {
createCookieSessionStorageFactory,
registerAuth,
registerRouter,
server: innerServer,
} = await server.setup(config);
const sessionStorageFactory = await createCookieSessionStorageFactory<StorageData>(
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<StorageData>((req, t) => {
const {
createCookieSessionStorageFactory,
registerAuth,
registerRouter,
server: innerServer,
} = await server.setup(config);
const sessionStorageFactory = await createCookieSessionStorageFactory<StorageData>(
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<StorageData>((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))

View file

@ -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: <T>(
cookieOptions: SessionStorageCookieOptions<T>
) => Promise<SessionStorageFactory<T>>;
/**
* 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: <T>(
handler: AuthenticationHandler,
cookieOptions: SessionStorageCookieOptions<T>
) => Promise<{ sessionStorageFactory: SessionStorageFactory<T> }>;
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<Router>();
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: <T>(fn: AuthenticationHandler, cookieOptions: SessionStorageCookieOptions<T>) =>
this.registerAuth(fn, cookieOptions, config.basePath),
createCookieSessionStorageFactory: <T>(cookieOptions: SessionStorageCookieOptions<T>) =>
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<T>(
fn: AuthenticationHandler,
private async createCookieSessionStorageFactory<T>(
cookieOptions: SessionStorageCookieOptions<T>,
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<T>(
this.logger.get('http', 'server', this.name, 'cookie-session-storage'),
this.server,
cookieOptions,
basePath
);
return sessionStorageFactory;
}
private registerAuth<T>(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<T>(
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 };
}
}

View file

@ -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;
};

View file

@ -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<StorageData>((req, t) => {
const sessionStorageFactory = await http.createCookieSessionStorageFactory<StorageData>(
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<StorageData>((req, t) => {
const sessionStorageFactory = await http.createCookieSessionStorageFactory<StorageData>(
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<StorageData>((req, t) => {
const sessionStorageFactory = await http.createCookieSessionStorageFactory<StorageData>(
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) => {

View file

@ -125,6 +125,7 @@ export interface CoreSetup {
) => ClusterClient;
};
http: {
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];

View file

@ -118,6 +118,7 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
createClient: deps.elasticsearch.createClient,
},
http: {
createCookieSessionStorageFactory: deps.http.createCookieSessionStorageFactory,
registerOnPreAuth: deps.http.registerOnPreAuth,
registerAuth: deps.http.registerAuth,
registerOnPostAuth: deps.http.registerOnPostAuth,

View file

@ -93,6 +93,7 @@ export interface CoreSetup {
};
// (undocumented)
http: {
createCookieSessionStorageFactory: HttpServiceSetup['createCookieSessionStorageFactory'];
registerOnPreAuth: HttpServiceSetup['registerOnPreAuth'];
registerAuth: HttpServiceSetup['registerAuth'];
registerOnPostAuth: HttpServiceSetup['registerOnPostAuth'];