Typesciptify Authenticator and move its tests to Jest. (#35750)

This commit is contained in:
Aleh Zasypkin 2019-05-02 13:32:45 +02:00 committed by GitHub
parent 9d2be1b730
commit 6871157963
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 709 additions and 700 deletions

21
x-pack/plugins/security/index.d.ts vendored Normal file
View file

@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Legacy } from 'kibana';
import { AuthenticatedUser } from './common/model';
import { AuthenticationResult, DeauthenticationResult } from './server/lib/authentication';
import { AuthorizationService } from './server/lib/authorization/service';
/**
* Public interface of the security plugin.
*/
export interface SecurityPlugin {
authorization: Readonly<AuthorizationService>;
authenticate: (request: Legacy.Request) => Promise<AuthenticationResult>;
deauthenticate: (request: Legacy.Request) => Promise<DeauthenticationResult>;
getUser: (request: Legacy.Request) => Promise<AuthenticatedUser>;
isAuthenticated: (request: Legacy.Request) => Promise<boolean>;
}

View file

@ -1,109 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import sinon from 'sinon';
import { AuthScopeService } from '../auth_scope_service';
async function assertReject(fn, check) {
try {
await fn();
throw new Error('expected function to reject the promise');
} catch (error) {
expect(() => { throw error; }).to.throwError(check);
}
}
describe('getCredentialsScope()', function () {
describe('basics', () => {
it('does not take any arguments', () => {
const authScope = new AuthScopeService();
expect(authScope).to.be.a(AuthScopeService);
});
});
describe('registerGetter()', () => {
it('throws when not passed a function', () => {
const authScope = new AuthScopeService();
expect(() => authScope.registerGetter()).to.throwError('function');
expect(() => authScope.registerGetter(null)).to.throwError('function');
expect(() => authScope.registerGetter(0)).to.throwError('function');
expect(() => authScope.registerGetter('')).to.throwError('function');
expect(() => authScope.registerGetter([])).to.throwError('function');
expect(() => authScope.registerGetter({})).to.throwError('function');
});
it('only calls the passed function when #getForRequestAndUser() is called', async () => {
const authScope = new AuthScopeService();
const stub = sinon.stub();
authScope.registerGetter(stub);
sinon.assert.notCalled(stub);
const request = {};
const user = {};
await authScope.getForRequestAndUser(request, user);
sinon.assert.calledOnce(stub);
expect(stub.firstCall.args[0]).to.be(request);
expect(stub.firstCall.args[1]).to.be(user);
});
});
describe('#getForRequestAndUser()', () => {
it('throws when request and user are not objects', async () => {
const authScope = new AuthScopeService();
await assertReject(() => authScope.getForRequestAndUser(null, {}), 'request object');
await assertReject(() => authScope.getForRequestAndUser(1, {}), 'request object');
await assertReject(() => authScope.getForRequestAndUser('abc', {}), 'request object');
await assertReject(() => authScope.getForRequestAndUser({}, null), 'user object');
await assertReject(() => authScope.getForRequestAndUser({}, 1), 'user object');
await assertReject(() => authScope.getForRequestAndUser({}, 'abc'), 'user object');
});
it('returns a promise for an empty array by default', async () => {
const authScope = new AuthScopeService();
const scope = await authScope.getForRequestAndUser({}, {});
expect(scope).to.eql([]);
});
it('calls each registered getter once each call', async () => {
const authScope = new AuthScopeService();
const getter1 = sinon.stub();
const getter2 = sinon.stub();
const getter3 = sinon.stub();
authScope.registerGetter(getter1);
authScope.registerGetter(getter2);
authScope.registerGetter(getter3);
await authScope.getForRequestAndUser({}, {});
sinon.assert.calledOnce(getter1);
sinon.assert.calledOnce(getter2);
sinon.assert.calledOnce(getter3);
await authScope.getForRequestAndUser({}, {});
sinon.assert.calledTwice(getter1);
sinon.assert.calledTwice(getter2);
sinon.assert.calledTwice(getter3);
await authScope.getForRequestAndUser({}, {});
sinon.assert.calledThrice(getter1);
sinon.assert.calledThrice(getter2);
sinon.assert.calledThrice(getter3);
});
it('casts the return value of the getters to an produce a flat, unique array', async () => {
const authScope = new AuthScopeService();
authScope.registerGetter(() => undefined);
authScope.registerGetter(() => null);
authScope.registerGetter(() => ['foo', undefined, 'bar']);
authScope.registerGetter(() => ['foo', 'foo1', 'foo2']);
authScope.registerGetter(() => 'bar2');
expect(await authScope.getForRequestAndUser({}, {})).to.eql(['foo', 'bar', 'foo1', 'foo2', 'bar2']);
});
});
});

View file

@ -0,0 +1,135 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import sinon from 'sinon';
import { AuthenticatedUser } from '../../common/model';
import { requestFixture } from './__tests__/__fixtures__/request';
import { AuthScopeService } from './auth_scope_service';
describe('getCredentialsScope()', function() {
describe('basics', () => {
it('does not take any arguments', () => {
const authScope = new AuthScopeService();
expect(authScope).toBeInstanceOf(AuthScopeService);
});
});
describe('registerGetter()', () => {
it('throws when not passed a function', () => {
const authScope = new AuthScopeService();
expect(() => authScope.registerGetter(undefined as any)).toThrowError(
'Expected `getterFunction` to be a function'
);
expect(() => authScope.registerGetter(null as any)).toThrowError(
'Expected `getterFunction` to be a function'
);
expect(() => authScope.registerGetter(0 as any)).toThrowError(
'Expected `getterFunction` to be a function'
);
expect(() => authScope.registerGetter('' as any)).toThrowError(
'Expected `getterFunction` to be a function'
);
expect(() => authScope.registerGetter([] as any)).toThrowError(
'Expected `getterFunction` to be a function'
);
expect(() => authScope.registerGetter({} as any)).toThrowError(
'Expected `getterFunction` to be a function'
);
});
it('only calls the passed function when #getForRequestAndUser() is called', async () => {
const authScope = new AuthScopeService();
const stub = sinon.stub();
authScope.registerGetter(stub);
sinon.assert.notCalled(stub);
const request = requestFixture();
const user = {} as AuthenticatedUser;
await authScope.getForRequestAndUser(request, user);
sinon.assert.calledOnce(stub);
sinon.assert.calledWithExactly(stub, request, user);
});
});
describe('#getForRequestAndUser()', () => {
it('throws when request and user are not objects', async () => {
const authScope = new AuthScopeService();
await expect(authScope.getForRequestAndUser(null as any, {} as any)).rejects.toThrowError(
'getCredentialsScope() requires a request object'
);
await expect(authScope.getForRequestAndUser(1 as any, {} as any)).rejects.toThrowError(
'getCredentialsScope() requires a request object'
);
await expect(authScope.getForRequestAndUser('abc' as any, {} as any)).rejects.toThrowError(
'getCredentialsScope() requires a request object'
);
await expect(authScope.getForRequestAndUser({} as any, null as any)).rejects.toThrowError(
'getCredentialsScope() requires a user object'
);
await expect(authScope.getForRequestAndUser({} as any, 1 as any)).rejects.toThrowError(
'getCredentialsScope() requires a user object'
);
await expect(authScope.getForRequestAndUser({} as any, 'abc' as any)).rejects.toThrowError(
'getCredentialsScope() requires a user object'
);
});
it('returns a promise for an empty array by default', async () => {
const authScope = new AuthScopeService();
const scope = await authScope.getForRequestAndUser(requestFixture(), {} as any);
expect(scope).toEqual([]);
});
it('calls each registered getter once each call', async () => {
const authScope = new AuthScopeService();
const user = {} as any;
const request = requestFixture();
const getter1 = sinon.stub();
const getter2 = sinon.stub();
const getter3 = sinon.stub();
authScope.registerGetter(getter1);
authScope.registerGetter(getter2);
authScope.registerGetter(getter3);
await authScope.getForRequestAndUser(request, user);
sinon.assert.calledOnce(getter1);
sinon.assert.calledOnce(getter2);
sinon.assert.calledOnce(getter3);
await authScope.getForRequestAndUser(request, user);
sinon.assert.calledTwice(getter1);
sinon.assert.calledTwice(getter2);
sinon.assert.calledTwice(getter3);
await authScope.getForRequestAndUser(request, user);
sinon.assert.calledThrice(getter1);
sinon.assert.calledThrice(getter2);
sinon.assert.calledThrice(getter3);
});
it('casts the return value of the getters to an produce a flat, unique array', async () => {
const authScope = new AuthScopeService();
authScope.registerGetter(() => undefined as any);
authScope.registerGetter(() => null as any);
authScope.registerGetter(() => ['foo', undefined, 'bar'] as any);
authScope.registerGetter(() => ['foo', 'foo1', 'foo2']);
authScope.registerGetter(() => 'bar2');
expect(await authScope.getForRequestAndUser(requestFixture(), {} as any)).toEqual([
'foo',
'bar',
'foo1',
'foo2',
'bar2',
]);
});
});
});

View file

@ -4,7 +4,11 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Legacy } from 'kibana';
import { uniq, flattenDeep } from 'lodash';
import { AuthenticatedUser } from '../../common/model';
export type ScopesGetter = (request: Legacy.Request, user: AuthenticatedUser) => string[] | string;
/**
* Manages the creation of the scopes attached to the credentials which result
@ -17,11 +21,9 @@ import { uniq, flattenDeep } from 'lodash';
* This service's primary reason for existing it to track the list of functions
* that have been provided by plugins to which should be used to create the scopes
* for requests.
*
* @type {AuthScopeService}
*/
export class AuthScopeService {
_getterFunctions = [];
private readonly getterFunctions: ScopesGetter[] = [];
/**
* Add a function that will be used to determine the list of scopes for a
@ -33,28 +35,27 @@ export class AuthScopeService {
* The function should return either an array of tags (strings) or a
* promise that resolves to an array of tags.
*
* @param {Function} getterFunction
* @param getterFunction Auth scope getter function.
*/
registerGetter(getterFunction) {
registerGetter(getterFunction: ScopesGetter) {
if (typeof getterFunction !== 'function') {
throw new TypeError('Expected `getterFunction` to be a function');
}
this._getterFunctions.push(getterFunction);
this.getterFunctions.push(getterFunction);
}
/**
* Determine the scope for a specific user/request. Hapi credentials (the
* result of a hapi auth scheme) can have a `scope` property that lists scope
* "tags" which can then be required by routes using the
* `route.config.auth.access.scope` property, or accessed in pre-functions,
* extensions, or route handlers as `request.auth.credentials.scope`.
*
* @param {Hapi.Request} request
* @param {Object} user user object from the security API
* @return {Array<string>} a list of scope tags
*/
async getForRequestAndUser(request, user) {
* Determine the scope for a specific user/request. Hapi credentials (the
* result of a hapi auth scheme) can have a `scope` property that lists scope
* "tags" which can then be required by routes using the
* `route.config.auth.access.scope` property, or accessed in pre-functions,
* extensions, or route handlers as `request.auth.credentials.scope`.
*
* @param request Request instance.
* @param user User object from the security API
*/
async getForRequestAndUser(request: Legacy.Request, user: AuthenticatedUser): Promise<string[]> {
if (!request || typeof request !== 'object') {
throw new TypeError('getCredentialsScope() requires a request object');
}
@ -63,10 +64,8 @@ export class AuthScopeService {
throw new TypeError('getCredentialsScope() requires a user object');
}
const getterResults = await Promise.all(
this._getterFunctions.map(fn => fn(request, user))
);
const getterResults = await Promise.all(this.getterFunctions.map(fn => fn(request, user)));
return uniq(flattenDeep(getterResults).filter(Boolean));
return uniq(flattenDeep<string>(getterResults).filter(Boolean));
}
}

View file

@ -4,30 +4,35 @@
* you may not use this file except in compliance with the Elastic License.
*/
import expect from '@kbn/expect';
import sinon from 'sinon';
import Boom from 'boom';
import { Legacy } from 'kibana';
import { serverFixture } from '../../__tests__/__fixtures__/server';
import { requestFixture } from '../../__tests__/__fixtures__/request';
import { Session } from '../session';
import { AuthScopeService } from '../../auth_scope_service';
import { LoginAttempt } from '../login_attempt';
import { initAuthenticator } from '../authenticator';
import * as ClientShield from '../../../../../../server/lib/get_client_shield';
import { serverFixture } from '../__tests__/__fixtures__/server';
import { requestFixture } from '../__tests__/__fixtures__/request';
import { AuthenticationResult } from './authentication_result';
import { DeauthenticationResult } from './deauthentication_result';
import { Session } from './session';
import { AuthScopeService } from '../auth_scope_service';
import { LoginAttempt } from './login_attempt';
import { initAuthenticator } from './authenticator';
import * as ClientShield from '../../../../../server/lib/get_client_shield';
describe('Authenticator', () => {
const sandbox = sinon.createSandbox();
let config;
let server;
let session;
let cluster;
let config: sinon.SinonStubbedInstance<Legacy.KibanaConfig>;
let server: ReturnType<typeof serverFixture>;
let session: sinon.SinonStubbedInstance<Session>;
let cluster: sinon.SinonStubbedInstance<{
callWithRequest: (request: ReturnType<typeof requestFixture>, ...args: any[]) => any;
callWithInternalUser: (...args: any[]) => any;
}>;
beforeEach(() => {
server = serverFixture();
session = sinon.createStubInstance(Session);
config = { get: sinon.stub() };
config = { get: sinon.stub(), has: sinon.stub() };
// Cluster is returned by `getClient` function that is wrapped into `once` making cluster
// a static singleton, so we should use sandbox to set/reset its behavior between tests.
@ -37,92 +42,74 @@ describe('Authenticator', () => {
server.config.returns(config);
server.register.yields();
sandbox.stub(Session, 'create').withArgs(server).returns(Promise.resolve(session));
sandbox.stub(AuthScopeService.prototype, 'getForRequestAndUser')
.returns(Promise.resolve([]));
sandbox
.stub(Session, 'create')
.withArgs(server as any)
.resolves(session as any);
sandbox.stub(AuthScopeService.prototype, 'getForRequestAndUser').resolves([]);
sandbox.useFakeTimers();
});
afterEach(() => {
sandbox.restore();
});
afterEach(() => sandbox.restore());
describe('initialization', () => {
it('fails if authentication providers are not configured.', async () => {
config.get.withArgs('xpack.security.authProviders').returns([]);
try {
await initAuthenticator(server);
expect().fail('`initAuthenticator` should fail.');
} catch(err) {
expect(err).to.be.a(Error);
expect(err.message).to.be(
'No authentication provider is configured. Verify `xpack.security.authProviders` config value.'
);
}
await expect(initAuthenticator(server as any)).rejects.toThrowError(
'No authentication provider is configured. Verify `xpack.security.authProviders` config value.'
);
});
it('fails if configured authentication provider is not known.', async () => {
config.get.withArgs('xpack.security.authProviders').returns(['super-basic']);
try {
await initAuthenticator(server);
expect().fail('`initAuthenticator` should fail.');
} catch(err) {
expect(err).to.be.a(Error);
expect(err.message).to.be('Unsupported authentication provider name: super-basic.');
}
await expect(initAuthenticator(server as any)).rejects.toThrowError(
'Unsupported authentication provider name: super-basic.'
);
});
});
describe('`authenticate` method', () => {
let authenticate;
let authenticate: (request: ReturnType<typeof requestFixture>) => Promise<AuthenticationResult>;
beforeEach(async () => {
config.get.withArgs('xpack.security.authProviders').returns(['basic']);
server.plugins.kibana.systemApi.isSystemApiRequest.returns(true);
session.clear.throws(new Error('`Session.clear` is not supposed to be called!'));
await initAuthenticator(server);
await initAuthenticator(server as any);
// Second argument will be a method we'd like to test.
authenticate = server.expose.withArgs('authenticate').firstCall.args[1];
});
it('fails if request is not provided.', async () => {
try {
await authenticate();
expect().fail('`authenticate` should fail.');
} catch(err) {
expect(err).to.be.a(Error);
expect(err.message).to.be('Request should be a valid object, was [undefined].');
}
await expect(authenticate(undefined as any)).rejects.toThrowError(
'Request should be a valid object, was [undefined].'
);
});
it('fails if any authentication providers fail.', async () => {
const request = requestFixture({ headers: { authorization: 'Basic ***' } });
session.get.withArgs(request).returns(Promise.resolve(null));
session.get.withArgs(request).resolves(null);
const failureReason = new Error('Not Authorized');
cluster.callWithRequest.withArgs(request).returns(Promise.reject(failureReason));
cluster.callWithRequest.withArgs(request).rejects(failureReason);
const authenticationResult = await authenticate(request);
expect(authenticationResult.failed()).to.be(true);
expect(authenticationResult.error).to.be(failureReason);
expect(authenticationResult.failed()).toBe(true);
expect(authenticationResult.error).toBe(failureReason);
});
it('returns user that authentication provider returns.', async () => {
const request = requestFixture({ headers: { authorization: 'Basic ***' } });
const user = { username: 'user' };
cluster.callWithRequest.withArgs(request).returns(Promise.resolve(user));
cluster.callWithRequest.withArgs(request).resolves(user);
const authenticationResult = await authenticate(request);
expect(authenticationResult.succeeded()).to.be(true);
expect(authenticationResult.user).to.be.eql({
...user,
scope: []
});
expect(authenticationResult.succeeded()).toBe(true);
expect(authenticationResult.user).toEqual({ ...user, scope: [] });
});
it('creates session whenever authentication provider returns state for system API requests', async () => {
@ -131,24 +118,19 @@ describe('Authenticator', () => {
const loginAttempt = new LoginAttempt();
const authorization = `Basic ${Buffer.from('foo:bar').toString('base64')}`;
loginAttempt.setCredentials('foo', 'bar');
request.loginAttempt.returns(loginAttempt);
(request.loginAttempt as sinon.SinonStub).returns(loginAttempt);
server.plugins.kibana.systemApi.isSystemApiRequest
.withArgs(request).returns(true);
server.plugins.kibana.systemApi.isSystemApiRequest.withArgs(request).returns(true);
cluster.callWithRequest
.withArgs(request).returns(Promise.resolve(user));
cluster.callWithRequest.withArgs(request).resolves(user);
const systemAPIAuthenticationResult = await authenticate(request);
expect(systemAPIAuthenticationResult.succeeded()).to.be(true);
expect(systemAPIAuthenticationResult.user).to.be.eql({
...user,
scope: []
});
expect(systemAPIAuthenticationResult.succeeded()).toBe(true);
expect(systemAPIAuthenticationResult.user).toEqual({ ...user, scope: [] });
sinon.assert.calledOnce(session.set);
sinon.assert.calledWithExactly(session.set, request, {
state: { authorization },
provider: 'basic'
provider: 'basic',
});
});
@ -158,24 +140,19 @@ describe('Authenticator', () => {
const loginAttempt = new LoginAttempt();
const authorization = `Basic ${Buffer.from('foo:bar').toString('base64')}`;
loginAttempt.setCredentials('foo', 'bar');
request.loginAttempt.returns(loginAttempt);
(request.loginAttempt as sinon.SinonStub).returns(loginAttempt);
server.plugins.kibana.systemApi.isSystemApiRequest
.withArgs(request).returns(false);
server.plugins.kibana.systemApi.isSystemApiRequest.withArgs(request).returns(false);
cluster.callWithRequest
.withArgs(request).returns(Promise.resolve(user));
cluster.callWithRequest.withArgs(request).resolves(user);
const notSystemAPIAuthenticationResult = await authenticate(request);
expect(notSystemAPIAuthenticationResult.succeeded()).to.be(true);
expect(notSystemAPIAuthenticationResult.user).to.be.eql({
...user,
scope: []
});
expect(notSystemAPIAuthenticationResult.succeeded()).toBe(true);
expect(notSystemAPIAuthenticationResult.user).toEqual({ ...user, scope: [] });
sinon.assert.calledOnce(session.set);
sinon.assert.calledWithExactly(session.set, request, {
state: { authorization },
provider: 'basic'
provider: 'basic',
});
});
@ -184,42 +161,40 @@ describe('Authenticator', () => {
const systemAPIRequest = requestFixture({ headers: { xCustomHeader: 'xxx' } });
const notSystemAPIRequest = requestFixture({ headers: { xCustomHeader: 'yyy' } });
session.get.withArgs(systemAPIRequest).returns(Promise.resolve({
session.get.withArgs(systemAPIRequest).resolves({
state: { authorization: 'Basic xxx' },
provider: 'basic'
}));
provider: 'basic',
});
session.get.withArgs(notSystemAPIRequest).returns(Promise.resolve({
session.get.withArgs(notSystemAPIRequest).resolves({
state: { authorization: 'Basic yyy' },
provider: 'basic'
}));
provider: 'basic',
});
server.plugins.kibana.systemApi.isSystemApiRequest
.withArgs(systemAPIRequest).returns(true)
.withArgs(notSystemAPIRequest).returns(false);
.withArgs(systemAPIRequest)
.returns(true)
.withArgs(notSystemAPIRequest)
.returns(false);
cluster.callWithRequest
.withArgs(systemAPIRequest).returns(Promise.resolve(user))
.withArgs(notSystemAPIRequest).returns(Promise.resolve(user));
.withArgs(systemAPIRequest)
.resolves(user)
.withArgs(notSystemAPIRequest)
.resolves(user);
const systemAPIAuthenticationResult = await authenticate(systemAPIRequest);
expect(systemAPIAuthenticationResult.succeeded()).to.be(true);
expect(systemAPIAuthenticationResult.user).to.be.eql({
...user,
scope: []
});
expect(systemAPIAuthenticationResult.succeeded()).toBe(true);
expect(systemAPIAuthenticationResult.user).toEqual({ ...user, scope: [] });
sinon.assert.notCalled(session.set);
const notSystemAPIAuthenticationResult = await authenticate(notSystemAPIRequest);
expect(notSystemAPIAuthenticationResult.succeeded()).to.be(true);
expect(notSystemAPIAuthenticationResult.user).to.be.eql({
...user,
scope: []
});
expect(notSystemAPIAuthenticationResult.succeeded()).toBe(true);
expect(notSystemAPIAuthenticationResult.user).toEqual({ ...user, scope: [] });
sinon.assert.calledOnce(session.set);
sinon.assert.calledWithExactly(session.set, notSystemAPIRequest, {
state: { authorization: 'Basic yyy' },
provider: 'basic'
provider: 'basic',
});
});
@ -227,29 +202,33 @@ describe('Authenticator', () => {
const systemAPIRequest = requestFixture({ headers: { xCustomHeader: 'xxx' } });
const notSystemAPIRequest = requestFixture({ headers: { xCustomHeader: 'yyy' } });
session.get.withArgs(systemAPIRequest).returns(Promise.resolve({
session.get.withArgs(systemAPIRequest).resolves({
state: { authorization: 'Basic xxx' },
provider: 'basic'
}));
provider: 'basic',
});
session.get.withArgs(notSystemAPIRequest).returns(Promise.resolve({
session.get.withArgs(notSystemAPIRequest).resolves({
state: { authorization: 'Basic yyy' },
provider: 'basic'
}));
provider: 'basic',
});
server.plugins.kibana.systemApi.isSystemApiRequest
.withArgs(systemAPIRequest).returns(true)
.withArgs(notSystemAPIRequest).returns(false);
.withArgs(systemAPIRequest)
.returns(true)
.withArgs(notSystemAPIRequest)
.returns(false);
cluster.callWithRequest
.withArgs(systemAPIRequest).returns(Promise.reject(new Error('some error')))
.withArgs(notSystemAPIRequest).returns(Promise.reject(new Error('some error')));
.withArgs(systemAPIRequest)
.rejects(new Error('some error'))
.withArgs(notSystemAPIRequest)
.rejects(new Error('some error'));
const systemAPIAuthenticationResult = await authenticate(systemAPIRequest);
expect(systemAPIAuthenticationResult.failed()).to.be(true);
expect(systemAPIAuthenticationResult.failed()).toBe(true);
const notSystemAPIAuthenticationResult = await authenticate(notSystemAPIRequest);
expect(notSystemAPIAuthenticationResult.failed()).to.be(true);
expect(notSystemAPIAuthenticationResult.failed()).toBe(true);
sinon.assert.notCalled(session.clear);
sinon.assert.notCalled(session.set);
@ -261,29 +240,24 @@ describe('Authenticator', () => {
const request = requestFixture();
const loginAttempt = new LoginAttempt();
loginAttempt.setCredentials('foo', 'bar');
request.loginAttempt.returns(loginAttempt);
(request.loginAttempt as sinon.SinonStub).returns(loginAttempt);
session.get.withArgs(request).returns(Promise.resolve({
session.get.withArgs(request).resolves({
state: { authorization: 'Basic some-old-token' },
provider: 'basic'
}));
provider: 'basic',
});
server.plugins.kibana.systemApi.isSystemApiRequest
.withArgs(request).returns(true);
server.plugins.kibana.systemApi.isSystemApiRequest.withArgs(request).returns(true);
cluster.callWithRequest
.withArgs(request).returns(Promise.resolve(user));
cluster.callWithRequest.withArgs(request).resolves(user);
const authenticationResult = await authenticate(request);
expect(authenticationResult.succeeded()).to.be(true);
expect(authenticationResult.user).to.be.eql({
...user,
scope: []
});
expect(authenticationResult.succeeded()).toBe(true);
expect(authenticationResult.user).toEqual({ ...user, scope: [] });
sinon.assert.calledOnce(session.set);
sinon.assert.calledWithExactly(session.set, request, {
state: { authorization },
provider: 'basic'
provider: 'basic',
});
});
@ -293,29 +267,24 @@ describe('Authenticator', () => {
const request = requestFixture();
const loginAttempt = new LoginAttempt();
loginAttempt.setCredentials('foo', 'bar');
request.loginAttempt.returns(loginAttempt);
(request.loginAttempt as sinon.SinonStub).returns(loginAttempt);
session.get.withArgs(request).returns(Promise.resolve({
session.get.withArgs(request).resolves({
state: { authorization: 'Basic some-old-token' },
provider: 'basic'
}));
provider: 'basic',
});
server.plugins.kibana.systemApi.isSystemApiRequest
.withArgs(request).returns(false);
server.plugins.kibana.systemApi.isSystemApiRequest.withArgs(request).returns(false);
cluster.callWithRequest
.withArgs(request).returns(Promise.resolve(user));
cluster.callWithRequest.withArgs(request).resolves(user);
const authenticationResult = await authenticate(request);
expect(authenticationResult.succeeded()).to.be(true);
expect(authenticationResult.user).to.be.eql({
...user,
scope: []
});
expect(authenticationResult.succeeded()).toBe(true);
expect(authenticationResult.user).toEqual({ ...user, scope: [] });
sinon.assert.calledOnce(session.set);
sinon.assert.calledWithExactly(session.set, request, {
state: { authorization },
provider: 'basic'
provider: 'basic',
});
});
@ -323,34 +292,38 @@ describe('Authenticator', () => {
const systemAPIRequest = requestFixture({ headers: { xCustomHeader: 'xxx' } });
const notSystemAPIRequest = requestFixture({ headers: { xCustomHeader: 'yyy' } });
session.get.withArgs(systemAPIRequest).returns(Promise.resolve({
session.get.withArgs(systemAPIRequest).resolves({
state: { authorization: 'Basic xxx' },
provider: 'basic'
}));
provider: 'basic',
});
session.get.withArgs(notSystemAPIRequest).returns(Promise.resolve({
session.get.withArgs(notSystemAPIRequest).resolves({
state: { authorization: 'Basic yyy' },
provider: 'basic'
}));
provider: 'basic',
});
session.clear.returns(Promise.resolve());
session.clear.resolves();
server.plugins.kibana.systemApi.isSystemApiRequest
.withArgs(systemAPIRequest).returns(true)
.withArgs(notSystemAPIRequest).returns(false);
.withArgs(systemAPIRequest)
.returns(true)
.withArgs(notSystemAPIRequest)
.returns(false);
cluster.callWithRequest
.withArgs(systemAPIRequest).returns(Promise.reject(Boom.unauthorized('token expired')))
.withArgs(notSystemAPIRequest).returns(Promise.reject(Boom.unauthorized('invalid token')));
.withArgs(systemAPIRequest)
.rejects(Boom.unauthorized('token expired'))
.withArgs(notSystemAPIRequest)
.rejects(Boom.unauthorized('invalid token'));
const systemAPIAuthenticationResult = await authenticate(systemAPIRequest);
expect(systemAPIAuthenticationResult.failed()).to.be(true);
expect(systemAPIAuthenticationResult.failed()).toBe(true);
sinon.assert.calledOnce(session.clear);
sinon.assert.calledWithExactly(session.clear, systemAPIRequest);
const notSystemAPIAuthenticationResult = await authenticate(notSystemAPIRequest);
expect(notSystemAPIAuthenticationResult.failed()).to.be(true);
expect(notSystemAPIAuthenticationResult.failed()).toBe(true);
sinon.assert.calledTwice(session.clear);
sinon.assert.calledWithExactly(session.clear, notSystemAPIRequest);
@ -359,97 +332,101 @@ describe('Authenticator', () => {
it('clears session if provider requested it via setting state to `null`.', async () => {
// Use `token` provider for this test as it's the only one that does what we want.
config.get.withArgs('xpack.security.authProviders').returns(['token']);
await initAuthenticator(server);
await initAuthenticator(server as any);
authenticate = server.expose.withArgs('authenticate').lastCall.args[1];
const request = requestFixture({ headers: { xCustomHeader: 'xxx' } });
session.get.withArgs(request).resolves({
state: { accessToken: 'access-xxx', refreshToken: 'refresh-xxx' },
provider: 'token'
provider: 'token',
});
session.clear.resolves();
cluster.callWithRequest
.withArgs(request).rejects({ statusCode: 401 });
cluster.callWithRequest.withArgs(request).rejects({ statusCode: 401 });
cluster.callWithInternalUser.withArgs('shield.getAccessToken').rejects(
Boom.badRequest('refresh token expired')
);
cluster.callWithInternalUser
.withArgs('shield.getAccessToken')
.rejects(Boom.badRequest('refresh token expired'));
const authenticationResult = await authenticate(request);
expect(authenticationResult.redirected()).to.be(true);
expect(authenticationResult.redirected()).toBe(true);
sinon.assert.calledOnce(session.clear);
sinon.assert.calledWithExactly(session.clear, request);
});
it('does not clear session if provider failed to authenticate request with non-401 reason with active session.',
async () => {
const systemAPIRequest = requestFixture({ headers: { xCustomHeader: 'xxx' } });
const notSystemAPIRequest = requestFixture({ headers: { xCustomHeader: 'yyy' } });
it('does not clear session if provider failed to authenticate request with non-401 reason with active session.', async () => {
const systemAPIRequest = requestFixture({ headers: { xCustomHeader: 'xxx' } });
const notSystemAPIRequest = requestFixture({ headers: { xCustomHeader: 'yyy' } });
session.get.withArgs(systemAPIRequest).returns(Promise.resolve({
state: { authorization: 'Basic xxx' },
provider: 'basic'
}));
session.get.withArgs(notSystemAPIRequest).returns(Promise.resolve({
state: { authorization: 'Basic yyy' },
provider: 'basic'
}));
session.clear.returns(Promise.resolve());
server.plugins.kibana.systemApi.isSystemApiRequest
.withArgs(systemAPIRequest).returns(true)
.withArgs(notSystemAPIRequest).returns(false);
cluster.callWithRequest
.withArgs(systemAPIRequest).returns(Promise.reject(Boom.badRequest('something went wrong')))
.withArgs(notSystemAPIRequest).returns(Promise.reject(new Error('Non boom error')));
const systemAPIAuthenticationResult = await authenticate(systemAPIRequest);
expect(systemAPIAuthenticationResult.failed()).to.be(true);
const notSystemAPIAuthenticationResult = await authenticate(notSystemAPIRequest);
expect(notSystemAPIAuthenticationResult.failed()).to.be(true);
sinon.assert.notCalled(session.clear);
session.get.withArgs(systemAPIRequest).resolves({
state: { authorization: 'Basic xxx' },
provider: 'basic',
});
session.get.withArgs(notSystemAPIRequest).resolves({
state: { authorization: 'Basic yyy' },
provider: 'basic',
});
session.clear.resolves();
server.plugins.kibana.systemApi.isSystemApiRequest
.withArgs(systemAPIRequest)
.returns(true)
.withArgs(notSystemAPIRequest)
.returns(false);
cluster.callWithRequest
.withArgs(systemAPIRequest)
.rejects(Boom.badRequest('something went wrong'))
.withArgs(notSystemAPIRequest)
.rejects(new Error('Non boom error'));
const systemAPIAuthenticationResult = await authenticate(systemAPIRequest);
expect(systemAPIAuthenticationResult.failed()).toBe(true);
const notSystemAPIAuthenticationResult = await authenticate(notSystemAPIRequest);
expect(notSystemAPIAuthenticationResult.failed()).toBe(true);
sinon.assert.notCalled(session.clear);
});
it('does not clear session if provider can not handle request authentication with active session.', async () => {
// Add `kbn-xsrf` header to the raw part of the request to make `can_redirect_request`
// think that it's AJAX request and redirect logic shouldn't be triggered.
const systemAPIRequest = requestFixture({
headers: { xCustomHeader: 'xxx', 'kbn-xsrf': 'xsrf' }
headers: { xCustomHeader: 'xxx', 'kbn-xsrf': 'xsrf' },
});
const notSystemAPIRequest = requestFixture({
headers: { xCustomHeader: 'yyy', 'kbn-xsrf': 'xsrf' }
headers: { xCustomHeader: 'yyy', 'kbn-xsrf': 'xsrf' },
});
session.get.withArgs(systemAPIRequest).returns(Promise.resolve({
session.get.withArgs(systemAPIRequest).resolves({
state: { authorization: 'Some weird authentication schema...' },
provider: 'basic'
}));
provider: 'basic',
});
session.get.withArgs(notSystemAPIRequest).returns(Promise.resolve({
session.get.withArgs(notSystemAPIRequest).resolves({
state: { authorization: 'Some weird authentication schema...' },
provider: 'basic'
}));
provider: 'basic',
});
session.clear.returns(Promise.resolve());
session.clear.resolves();
server.plugins.kibana.systemApi.isSystemApiRequest
.withArgs(systemAPIRequest).returns(true)
.withArgs(notSystemAPIRequest).returns(false);
.withArgs(systemAPIRequest)
.returns(true)
.withArgs(notSystemAPIRequest)
.returns(false);
const systemAPIAuthenticationResult = await authenticate(systemAPIRequest);
expect(systemAPIAuthenticationResult.failed()).to.be(true);
expect(systemAPIAuthenticationResult.failed()).toBe(true);
const notSystemAPIAuthenticationResult = await authenticate(notSystemAPIRequest);
expect(notSystemAPIAuthenticationResult.failed()).to.be(true);
expect(notSystemAPIAuthenticationResult.failed()).toBe(true);
sinon.assert.notCalled(session.clear);
});
@ -458,34 +435,36 @@ describe('Authenticator', () => {
// Add `kbn-xsrf` header to the raw part of the request to make `can_redirect_request`
// think that it's AJAX request and redirect logic shouldn't be triggered.
const systemAPIRequest = requestFixture({
headers: { xCustomHeader: 'xxx', 'kbn-xsrf': 'xsrf' }
headers: { xCustomHeader: 'xxx', 'kbn-xsrf': 'xsrf' },
});
const notSystemAPIRequest = requestFixture({
headers: { xCustomHeader: 'yyy', 'kbn-xsrf': 'xsrf' }
headers: { xCustomHeader: 'yyy', 'kbn-xsrf': 'xsrf' },
});
session.get.withArgs(systemAPIRequest).resolves({
state: { accessToken: 'some old token' },
provider: 'token'
provider: 'token',
});
session.get.withArgs(notSystemAPIRequest).resolves({
state: { accessToken: 'some old token' },
provider: 'token'
provider: 'token',
});
session.clear.resolves();
server.plugins.kibana.systemApi.isSystemApiRequest
.withArgs(systemAPIRequest).returns(true)
.withArgs(notSystemAPIRequest).returns(false);
.withArgs(systemAPIRequest)
.returns(true)
.withArgs(notSystemAPIRequest)
.returns(false);
const systemAPIAuthenticationResult = await authenticate(systemAPIRequest);
expect(systemAPIAuthenticationResult.notHandled()).to.be(true);
expect(systemAPIAuthenticationResult.notHandled()).toBe(true);
sinon.assert.calledOnce(session.clear);
const notSystemAPIAuthenticationResult = await authenticate(notSystemAPIRequest);
expect(notSystemAPIAuthenticationResult.notHandled()).to.be(true);
expect(notSystemAPIAuthenticationResult.notHandled()).toBe(true);
sinon.assert.calledTwice(session.clear);
});
@ -493,88 +472,85 @@ describe('Authenticator', () => {
const user = { username: 'user' };
const request = requestFixture({ headers: { authorization: 'Basic ***' } });
cluster.callWithRequest.withArgs(request)
.returns(Promise.resolve(user));
AuthScopeService.prototype.getForRequestAndUser.withArgs(request, user)
.returns(Promise.resolve(['foo', 'bar']));
cluster.callWithRequest.withArgs(request).resolves(user);
(AuthScopeService.prototype.getForRequestAndUser as sinon.SinonStub)
.withArgs(request, user)
.resolves(['foo', 'bar']);
const authenticationResult = await authenticate(request);
expect(authenticationResult.succeeded()).to.be(true);
expect(authenticationResult.user).to.be.eql({
...user,
scope: ['foo', 'bar']
});
expect(authenticationResult.succeeded()).toBe(true);
expect(authenticationResult.user).toEqual({ ...user, scope: ['foo', 'bar'] });
});
});
describe('`deauthenticate` method', () => {
let deauthenticate;
let deauthenticate: (
request: ReturnType<typeof requestFixture>
) => Promise<DeauthenticationResult>;
beforeEach(async () => {
config.get.withArgs('xpack.security.authProviders').returns(['basic']);
config.get.withArgs('server.basePath').returns('/base-path');
await initAuthenticator(server);
await initAuthenticator(server as any);
// Second argument will be a method we'd like to test.
deauthenticate = server.expose.withArgs('deauthenticate').firstCall.args[1];
});
it('fails if request is not provided.', async () => {
try {
await deauthenticate();
expect().fail('`deauthenticate` should fail.');
} catch(err) {
expect(err).to.be.a(Error);
expect(err.message).to.be('Request should be a valid object, was [undefined].');
}
await expect(deauthenticate(undefined as any)).rejects.toThrowError(
'Request should be a valid object, was [undefined].'
);
});
it('returns `notHandled` if session does not exist.', async () => {
const request = requestFixture();
session.get.withArgs(request).returns(null);
session.get.withArgs(request).resolves(null);
const deauthenticationResult = await deauthenticate(request);
expect(deauthenticationResult.notHandled()).to.be(true);
expect(deauthenticationResult.notHandled()).toBe(true);
sinon.assert.notCalled(session.clear);
});
it('clears session and returns whatever authentication provider returns.', async () => {
const request = requestFixture({ search: '?next=%2Fapp%2Fml&msg=SESSION_EXPIRED' });
session.get.withArgs(request).returns(Promise.resolve({
state: {},
provider: 'basic'
}));
const deauthenticationResult = await deauthenticate(request);
sinon.assert.calledOnce(session.clear);
sinon.assert.calledWithExactly(session.clear, request);
expect(deauthenticationResult.redirected()).to.be(true);
expect(deauthenticationResult.redirectURL).to.be('/base-path/login?next=%2Fapp%2Fml&msg=SESSION_EXPIRED');
});
it('only clears session if it belongs to not configured provider.', async () => {
const request = requestFixture({ search: '?next=%2Fapp%2Fml&msg=SESSION_EXPIRED' });
session.get.withArgs(request).resolves({
state: {},
provider: 'token'
provider: 'basic',
});
const deauthenticationResult = await deauthenticate(request);
sinon.assert.calledOnce(session.clear);
sinon.assert.calledWithExactly(session.clear, request);
expect(deauthenticationResult.notHandled()).to.be(true);
expect(deauthenticationResult.redirected()).toBe(true);
expect(deauthenticationResult.redirectURL).toBe(
'/base-path/login?next=%2Fapp%2Fml&msg=SESSION_EXPIRED'
);
});
it('only clears session if it belongs to not configured provider.', async () => {
const request = requestFixture({ search: '?next=%2Fapp%2Fml&msg=SESSION_EXPIRED' });
session.get.withArgs(request).resolves({
state: {},
provider: 'token',
});
const deauthenticationResult = await deauthenticate(request);
sinon.assert.calledOnce(session.clear);
sinon.assert.calledWithExactly(session.clear, request);
expect(deauthenticationResult.notHandled()).toBe(true);
});
});
describe('`isAuthenticated` method', () => {
let isAuthenticated;
let isAuthenticated: (request: ReturnType<typeof requestFixture>) => Promise<boolean>;
beforeEach(async () => {
config.get.withArgs('xpack.security.authProviders').returns(['basic']);
await initAuthenticator(server);
await initAuthenticator(server as any);
// Second argument will be a method we'd like to test.
isAuthenticated = server.expose.withArgs('isAuthenticated').firstCall.args[1];
@ -582,50 +558,32 @@ describe('Authenticator', () => {
it('returns `true` if `getUser` succeeds.', async () => {
const request = requestFixture();
server.plugins.security.getUser
.withArgs(request)
.returns(Promise.resolve({}));
server.plugins.security.getUser.withArgs(request).resolves({});
expect(await isAuthenticated(request)).to.be(true);
await expect(isAuthenticated(request)).resolves.toBe(true);
});
it('returns `false` when `getUser` throws a 401 boom error.', async () => {
const request = requestFixture();
server.plugins.security.getUser
.withArgs(request)
.returns(Promise.reject(Boom.unauthorized()));
server.plugins.security.getUser.withArgs(request).rejects(Boom.unauthorized());
expect(await isAuthenticated(request)).to.be(false);
await expect(isAuthenticated(request)).resolves.toBe(false);
});
it('throw non-boom errors.', async () => {
const request = requestFixture();
const nonBoomError = new TypeError();
server.plugins.security.getUser
.withArgs(request)
.returns(Promise.reject(nonBoomError));
server.plugins.security.getUser.withArgs(request).rejects(nonBoomError);
try {
await isAuthenticated(request);
throw new Error('`isAuthenticated` should throw.');
} catch (err) {
expect(err).to.be(nonBoomError);
}
await expect(isAuthenticated(request)).rejects.toThrowError(nonBoomError);
});
it('throw non-401 boom errors.', async () => {
const request = requestFixture();
const non401Error = Boom.boomify(new TypeError());
server.plugins.security.getUser
.withArgs(request)
.returns(Promise.reject(non401Error));
server.plugins.security.getUser.withArgs(request).rejects(non401Error);
try {
await isAuthenticated(request);
throw new Error('`isAuthenticated` should throw.');
} catch (err) {
expect(err).to.be(non401Error);
}
await expect(isAuthenticated(request)).rejects.toThrowError(non401Error);
});
});
});

View file

@ -4,26 +4,39 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Legacy } from 'kibana';
import { getClient } from '../../../../../server/lib/get_client_shield';
import { AuthScopeService } from '../auth_scope_service';
import { AuthScopeService, ScopesGetter } from '../auth_scope_service';
import { getErrorStatusCode } from '../errors';
import { BasicAuthenticationProvider } from './providers/basic';
import { SAMLAuthenticationProvider } from './providers/saml';
import { TokenAuthenticationProvider } from './providers/token';
import {
AuthenticationProviderOptions,
BaseAuthenticationProvider,
BasicAuthenticationProvider,
SAMLAuthenticationProvider,
TokenAuthenticationProvider,
} from './providers';
import { AuthenticationResult } from './authentication_result';
import { DeauthenticationResult } from './deauthentication_result';
import { Session } from './session';
import { LoginAttempt } from './login_attempt';
interface ProviderSession {
provider: string;
state: unknown;
}
// Mapping between provider key defined in the config and authentication
// provider class that can handle specific authentication mechanism.
const providerMap = new Map([
const providerMap = new Map<
string,
new (options: AuthenticationProviderOptions) => BaseAuthenticationProvider
>([
['basic', BasicAuthenticationProvider],
['saml', SAMLAuthenticationProvider],
['token', TokenAuthenticationProvider],
]);
function assertRequest(request) {
function assertRequest(request: Legacy.Request) {
if (!request || typeof request !== 'object') {
throw new Error(`Request should be a valid object, was [${typeof request}].`);
}
@ -31,10 +44,9 @@ function assertRequest(request) {
/**
* Prepares options object that is shared among all authentication providers.
* @param {Hapi.Server} server HapiJS Server instance.
* @returns {Object}
* @param server Server instance.
*/
function getProviderOptions(server) {
function getProviderOptions(server: Legacy.Server) {
const config = server.config();
return {
@ -42,14 +54,28 @@ function getProviderOptions(server) {
log: server.log.bind(server),
protocol: server.info.protocol,
hostname: config.get('server.host'),
port: config.get('server.port'),
basePath: config.get('server.basePath'),
hostname: config.get<string>('server.host'),
port: config.get<number>('server.port'),
basePath: config.get<string>('server.basePath'),
...config.get('xpack.security.public')
...config.get('xpack.security.public'),
};
}
/**
* Instantiates authentication provider based on the provider key from config.
* @param providerType Provider type key.
* @param options Options to pass to provider's constructor.
*/
function instantiateProvider(providerType: string, options: AuthenticationProviderOptions) {
const ProviderClassName = providerMap.get(providerType);
if (!ProviderClassName) {
throw new Error(`Unsupported authentication provider name: ${providerType}.`);
}
return new ProviderClassName(options);
}
/**
* Authenticator is responsible for authentication of the request using chain of
* authentication providers. The chain is essentially a prioritized list of configured
@ -64,47 +90,24 @@ function getProviderOptions(server) {
* will be returned.
*/
class Authenticator {
/**
* HapiJS server instance.
* @type {Hapi.Server}
* @private
*/
_server = null;
/**
* Service that gathers all `scopes` for particular user.
* @type {AuthScopeService}
* @private
*/
_authScope = null;
/**
* List of configured and instantiated authentication providers.
* @type {Map.<string, Object>}
* @private
*/
_providers = null;
/**
* Session class instance.
* @type {Session}
* @private
*/
_session = null;
private readonly providers: Map<string, BaseAuthenticationProvider>;
/**
* Instantiates Authenticator and bootstrap configured providers.
* @param {Hapi.Server} server HapiJS Server instance.
* @param {AuthScopeService} authScope AuthScopeService instance.
* @param {Session} session Session instance.
* @param server Server instance.
* @param authScope AuthScopeService instance.
* @param session Session instance.
*/
constructor(server, authScope, session) {
this._server = server;
this._authScope = authScope;
this._session = session;
const config = this._server.config();
const authProviders = config.get('xpack.security.authProviders');
constructor(
private readonly server: Legacy.Server,
private readonly authScope: AuthScopeService,
private readonly session: Session
) {
const config = this.server.config();
const authProviders = config.get<string[]>('xpack.security.authProviders');
if (authProviders.length === 0) {
throw new Error(
'No authentication provider is configured. Verify `xpack.security.authProviders` config value.'
@ -113,50 +116,55 @@ class Authenticator {
const providerOptions = Object.freeze(getProviderOptions(server));
this._providers = new Map(
this.providers = new Map(
authProviders.map(
(providerType) => [providerType, this._instantiateProvider(providerType, providerOptions)]
providerType =>
[providerType, instantiateProvider(providerType, providerOptions)] as [
string,
BaseAuthenticationProvider
]
)
);
}
/**
* Performs request authentication using configured chain of authentication providers.
* @param {Hapi.Request} request HapiJS request instance.
* @returns {Promise.<AuthenticationResult>}.
* @param request Request instance.
*/
async authenticate(request) {
async authenticate(request: Legacy.Request) {
assertRequest(request);
const isSystemApiRequest = this._server.plugins.kibana.systemApi.isSystemApiRequest(request);
const existingSession = await this._getSessionValue(request);
const isSystemApiRequest = this.server.plugins.kibana.systemApi.isSystemApiRequest(request);
const existingSession = await this.getSessionValue(request);
let authenticationResult;
for (const [providerType, provider] of this._providerIterator(existingSession)) {
for (const [providerType, provider] of this.providerIterator(existingSession)) {
// Check if current session has been set by this provider.
const ownsSession = existingSession && existingSession.provider === providerType;
authenticationResult = await provider.authenticate(
request,
ownsSession ? existingSession.state : null
ownsSession ? existingSession!.state : null
);
if (ownsSession || authenticationResult.shouldUpdateState()) {
// If authentication succeeds or requires redirect we should automatically extend existing user session,
// unless authentication has been triggered by a system API request. In case provider explicitly returns new
// state we should store it in the session regardless of whether it's a system API request or not.
const sessionCanBeUpdated = (authenticationResult.succeeded() || authenticationResult.redirected())
&& (authenticationResult.shouldUpdateState() || !isSystemApiRequest);
const sessionCanBeUpdated =
(authenticationResult.succeeded() || authenticationResult.redirected()) &&
(authenticationResult.shouldUpdateState() || !isSystemApiRequest);
// If provider owned the session, but failed to authenticate anyway, that likely means that
// session is not valid and we should clear it. Also provider can specifically ask to clear
// session by setting it to `null` even if authentication attempt didn't fail.
if (authenticationResult.shouldClearState() || (
authenticationResult.failed() && getErrorStatusCode(authenticationResult.error) === 401)
if (
authenticationResult.shouldClearState() ||
(authenticationResult.failed() && getErrorStatusCode(authenticationResult.error) === 401)
) {
await this._session.clear(request);
await this.session.clear(request);
} else if (sessionCanBeUpdated) {
await this._session.set(
await this.session.set(
request,
authenticationResult.shouldUpdateState()
? { state: authenticationResult.state, provider: providerType }
@ -173,8 +181,8 @@ class Authenticator {
return AuthenticationResult.succeeded({
...authenticationResult.user,
// Complement user returned from the provider with scopes.
scope: await this._authScope.getForRequestAndUser(request, authenticationResult.user)
});
scope: await this.authScope.getForRequestAndUser(request, authenticationResult.user!),
} as any);
} else if (authenticationResult.redirected()) {
return authenticationResult;
}
@ -185,17 +193,16 @@ class Authenticator {
/**
* Deauthenticates current request.
* @param {Hapi.Request} request HapiJS request instance.
* @returns {Promise.<DeauthenticationResult>}
* @param request Request instance.
*/
async deauthenticate(request) {
async deauthenticate(request: Legacy.Request) {
assertRequest(request);
const sessionValue = await this._getSessionValue(request);
const sessionValue = await this.getSessionValue(request);
if (sessionValue) {
await this._session.clear(request);
await this.session.clear(request);
return this._providers.get(sessionValue.provider).deauthenticate(request, sessionValue.state);
return this.providers.get(sessionValue.provider)!.deauthenticate(request, sessionValue.state);
}
// Normally when there is no active session in Kibana, `deauthenticate` method shouldn't do anything
@ -204,44 +211,29 @@ class Authenticator {
// SP associated with the current user session to do the logout. So if Kibana (without active session)
// receives such a request it shouldn't redirect user to the home page, but rather redirect back to IdP
// with correct logout response and only Elasticsearch knows how to do that.
if (request.query.SAMLRequest && this._providers.has('saml')) {
return this._providers.get('saml').deauthenticate(request);
if ((request.query as Record<string, string>).SAMLRequest && this.providers.has('saml')) {
return this.providers.get('saml')!.deauthenticate(request);
}
return DeauthenticationResult.notHandled();
}
/**
* Instantiates authentication provider based on the provider key from config.
* @param {string} providerType Provider type key.
* @param {Object} options Options to pass to provider's constructor.
* @returns {Object} Authentication provider instance.
* @private
*/
_instantiateProvider(providerType, options) {
const ProviderClassName = providerMap.get(providerType);
if (!ProviderClassName) {
throw new Error(`Unsupported authentication provider name: ${providerType}.`);
}
return new ProviderClassName(options);
}
/**
* Returns provider iterator where providers are sorted in the order of priority (based on the session ownership).
* @param {Object} sessionValue Current session value.
* @returns {Iterator.<Object>}
* @param sessionValue Current session value.
*/
*_providerIterator(sessionValue) {
*providerIterator(
sessionValue: ProviderSession | null
): IterableIterator<[string, BaseAuthenticationProvider]> {
// If there is no session to predict which provider to use first, let's use the order
// providers are configured in. Otherwise return provider that owns session first, and only then the rest
// of providers.
if (!sessionValue) {
yield* this._providers;
yield* this.providers;
} else {
yield [sessionValue.provider, this._providers.get(sessionValue.provider)];
yield [sessionValue.provider, this.providers.get(sessionValue.provider)!];
for (const [providerType, provider] of this._providers) {
for (const [providerType, provider] of this.providers) {
if (providerType !== sessionValue.provider) {
yield [providerType, provider];
}
@ -252,18 +244,16 @@ class Authenticator {
/**
* Extracts session value for the specified request. Under the hood it can
* clear session if it belongs to the provider that is not available.
* @param {Hapi.Request} request Request to extract session value for.
* @returns {Promise.<Object|null>}
* @private
* @param request Request to extract session value for.
*/
async _getSessionValue(request) {
let sessionValue = await this._session.get(request);
private async getSessionValue(request: Legacy.Request) {
let sessionValue = await this.session.get<ProviderSession>(request);
// If for some reason we have a session stored for the provider that is not available
// (e.g. when user was logged in with one provider, but then configuration has changed
// and that provider is no longer available), then we should clear session entirely.
if (sessionValue && !this._providers.has(sessionValue.provider)) {
await this._session.clear(request);
if (sessionValue && !this.providers.has(sessionValue.provider)) {
await this.session.clear(request);
sessionValue = null;
}
@ -271,13 +261,13 @@ class Authenticator {
}
}
export async function initAuthenticator(server) {
export async function initAuthenticator(server: Legacy.Server) {
const session = await Session.create(server);
const authScope = new AuthScopeService();
const authenticator = new Authenticator(server, authScope, session);
const loginAttempts = new WeakMap();
server.decorate('request', 'loginAttempt', function () {
server.decorate('request', 'loginAttempt', function(this: Legacy.Request) {
const request = this;
if (!loginAttempts.has(request)) {
loginAttempts.set(request, new LoginAttempt());
@ -285,13 +275,17 @@ export async function initAuthenticator(server) {
return loginAttempts.get(request);
});
server.expose('authenticate', (request) => authenticator.authenticate(request));
server.expose('deauthenticate', (request) => authenticator.deauthenticate(request));
server.expose('registerAuthScopeGetter', (scopeExtender) => authScope.registerGetter(scopeExtender));
server.expose('authenticate', (request: Legacy.Request) => authenticator.authenticate(request));
server.expose('deauthenticate', (request: Legacy.Request) =>
authenticator.deauthenticate(request)
);
server.expose('registerAuthScopeGetter', (scopeExtender: ScopesGetter) =>
authScope.registerGetter(scopeExtender)
);
server.expose('isAuthenticated', async (request) => {
server.expose('isAuthenticated', async (request: Legacy.Request) => {
try {
await server.plugins.security.getUser(request);
await server.plugins.security!.getUser(request);
return true;
} catch (err) {
// Don't swallow server errors.

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { AuthenticationResult } from './authentication_result';
export { DeauthenticationResult } from './deauthentication_result';

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { stub } from 'sinon';
import { AuthenticationProviderOptions } from './base';
export function mockAuthenticationProviderOptions(
providerOptions: Partial<AuthenticationProviderOptions> = {}
) {
return {
hostname: 'test-hostname',
port: 1234,
protocol: 'test-protocol',
client: { callWithRequest: stub(), callWithInternalUser: stub() },
log: stub(),
basePath: '/base-path',
...providerOptions,
};
}

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Legacy } from 'kibana';
import { AuthenticationResult } from '../authentication_result';
import { DeauthenticationResult } from '../deauthentication_result';
/**
* Represents available provider options.
*/
export interface AuthenticationProviderOptions {
protocol: string;
hostname: string;
port: number;
basePath: string;
client: Legacy.Plugins.elasticsearch.Cluster;
log: (tags: string[], message: string) => void;
}
/**
* Base class that all authentication providers should extend.
*/
export abstract class BaseAuthenticationProvider {
/**
* Instantiates AuthenticationProvider.
* @param options Provider options object.
*/
constructor(protected readonly options: Readonly<AuthenticationProviderOptions>) {}
/**
* Performs request authentication.
* @param request Request instance.
* @param [state] Optional state object associated with the provider.
*/
abstract authenticate(request: Legacy.Request, state?: unknown): Promise<AuthenticationResult>;
/**
* Invalidates user session associated with the request.
* @param request Request instance.
* @param [state] Optional state object associated with the provider that needs to be invalidated.
*/
abstract deauthenticate(
request: Legacy.Request,
state?: unknown
): Promise<DeauthenticationResult>;
}

View file

@ -7,6 +7,7 @@
import sinon from 'sinon';
import { requestFixture } from '../../__tests__/__fixtures__/request';
import { LoginAttempt } from '../login_attempt';
import { mockAuthenticationProviderOptions } from './base.mock';
import { BasicAuthenticationProvider, BasicCredentials } from './basic';
function generateAuthorizationHeader(username: string, password: string) {
@ -22,14 +23,9 @@ describe('BasicAuthenticationProvider', () => {
let provider: BasicAuthenticationProvider;
let callWithRequest: sinon.SinonStub;
beforeEach(() => {
callWithRequest = sinon.stub();
provider = new BasicAuthenticationProvider({
client: { callWithRequest } as any,
log() {
// no-op
},
basePath: '/base-path',
});
const providerOptions = mockAuthenticationProviderOptions();
callWithRequest = providerOptions.client.callWithRequest as sinon.SinonStub;
provider = new BasicAuthenticationProvider(providerOptions);
});
it('does not redirect AJAX requests that can not be authenticated to the login page.', async () => {
@ -176,13 +172,7 @@ describe('BasicAuthenticationProvider', () => {
describe('`deauthenticate` method', () => {
let provider: BasicAuthenticationProvider;
beforeEach(() => {
provider = new BasicAuthenticationProvider({
client: { callWithRequest: sinon.stub() } as any,
log() {
// no-op
},
basePath: '/base-path',
});
provider = new BasicAuthenticationProvider(mockAuthenticationProviderOptions());
});
it('always redirects to the login page.', async () => {

View file

@ -6,12 +6,12 @@
/* eslint-disable max-classes-per-file */
import { Request } from 'hapi';
import { Cluster } from 'src/legacy/core_plugins/elasticsearch';
import { Legacy } from 'kibana';
import { canRedirectRequest } from '../../can_redirect_request';
import { AuthenticationResult } from '../authentication_result';
import { DeauthenticationResult } from '../deauthentication_result';
import { LoginAttempt } from '../login_attempt';
import { BaseAuthenticationProvider } from './base';
/**
* Utility class that knows how to decorate request with proper Basic authentication headers.
@ -20,11 +20,15 @@ export class BasicCredentials {
/**
* Takes provided `username` and `password`, transforms them into proper `Basic ***` authorization
* header and decorates passed request with it.
* @param request HapiJS request instance.
* @param request Request instance.
* @param username User name.
* @param password User password.
*/
public static decorateRequest<T extends Request>(request: T, username: string, password: string) {
public static decorateRequest<T extends Legacy.Request>(
request: T,
username: string,
password: string
) {
const typeOfRequest = typeof request;
if (!request || typeOfRequest !== 'object') {
throw new Error('Request should be a valid object.');
@ -44,19 +48,10 @@ export class BasicCredentials {
}
}
type RequestWithLoginAttempt = Request & {
type RequestWithLoginAttempt = Legacy.Request & {
loginAttempt: () => LoginAttempt;
};
/**
* Represents available provider options.
*/
interface ProviderOptions {
basePath: string;
client: Cluster;
log: (tags: string[], message: string) => void;
}
/**
* The state supported by the provider.
*/
@ -72,16 +67,10 @@ interface ProviderState {
/**
* Provider that supports request authentication via Basic HTTP Authentication.
*/
export class BasicAuthenticationProvider {
/**
* Instantiates BasicAuthenticationProvider.
* @param options Provider options object.
*/
constructor(private readonly options: ProviderOptions) {}
export class BasicAuthenticationProvider extends BaseAuthenticationProvider {
/**
* Performs request authentication using Basic HTTP Authentication.
* @param request HapiJS request instance.
* @param request Request instance.
* @param [state] Optional state object associated with the provider.
*/
public async authenticate(request: RequestWithLoginAttempt, state?: ProviderState | null) {
@ -117,9 +106,9 @@ export class BasicAuthenticationProvider {
/**
* Redirects user to the login page preserving query string parameters.
* @param request HapiJS request instance.
* @param request Request instance.
*/
public async deauthenticate(request: Request) {
public async deauthenticate(request: Legacy.Request) {
// Query string may contain the path where logout has been called or
// logout reason that login page may need to know.
return DeauthenticationResult.redirectTo(
@ -130,7 +119,7 @@ export class BasicAuthenticationProvider {
/**
* Validates whether request contains a login payload and authenticates the
* user if necessary.
* @param request HapiJS request instance.
* @param request Request instance.
*/
private async authenticateViaLoginAttempt(request: RequestWithLoginAttempt) {
this.debug('Trying to authenticate via login attempt.');
@ -162,9 +151,9 @@ export class BasicAuthenticationProvider {
/**
* Validates whether request contains `Basic ***` Authorization header and just passes it
* forward to Elasticsearch backend.
* @param request HapiJS request instance.
* @param request Request instance.
*/
private async authenticateViaHeader(request: Request) {
private async authenticateViaHeader(request: Legacy.Request) {
this.debug('Trying to authenticate via header.');
const authorization = request.headers.authorization;
@ -197,10 +186,10 @@ export class BasicAuthenticationProvider {
/**
* Tries to extract authorization header from the state and adds it to the request before
* it's forwarded to Elasticsearch backend.
* @param request HapiJS request instance.
* @param request Request instance.
* @param state State value previously stored by the provider.
*/
private async authenticateViaState(request: Request, { authorization }: ProviderState) {
private async authenticateViaState(request: Legacy.Request, { authorization }: ProviderState) {
this.debug('Trying to authenticate via state.');
if (!authorization) {

View file

@ -0,0 +1,10 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { BaseAuthenticationProvider, AuthenticationProviderOptions } from './base';
export { BasicAuthenticationProvider, BasicCredentials } from './basic';
export { SAMLAuthenticationProvider } from './saml';
export { TokenAuthenticationProvider } from './token';

View file

@ -8,6 +8,7 @@ import Boom from 'boom';
import sinon from 'sinon';
import { requestFixture } from '../../__tests__/__fixtures__/request';
import { mockAuthenticationProviderOptions } from './base.mock';
import { SAMLAuthenticationProvider } from './saml';
@ -16,19 +17,11 @@ describe('SAMLAuthenticationProvider', () => {
let callWithRequest: sinon.SinonStub;
let callWithInternalUser: sinon.SinonStub;
beforeEach(() => {
callWithRequest = sinon.stub();
callWithInternalUser = sinon.stub();
const providerOptions = mockAuthenticationProviderOptions({ basePath: '/test-base-path' });
callWithRequest = providerOptions.client.callWithRequest as sinon.SinonStub;
callWithInternalUser = providerOptions.client.callWithInternalUser as sinon.SinonStub;
provider = new SAMLAuthenticationProvider({
client: { callWithRequest, callWithInternalUser } as any,
log() {
// no-op
},
protocol: 'test-protocol',
hostname: 'test-hostname',
port: 1234,
basePath: '/test-base-path',
});
provider = new SAMLAuthenticationProvider(providerOptions);
});
describe('`authenticate` method', () => {

View file

@ -5,25 +5,13 @@
*/
import Boom from 'boom';
import { Request } from 'hapi';
import { Cluster } from 'src/legacy/core_plugins/elasticsearch';
import { Legacy } from 'kibana';
import { canRedirectRequest } from '../../can_redirect_request';
import { getErrorStatusCode } from '../../errors';
import { AuthenticatedUser } from '../../../../common/model';
import { AuthenticationResult } from '../authentication_result';
import { DeauthenticationResult } from '../deauthentication_result';
/**
* Represents available provider options.
*/
interface ProviderOptions {
protocol: string;
hostname: string;
port: number;
basePath: string;
client: Cluster;
log: (tags: string[], message: string) => void;
}
import { BaseAuthenticationProvider } from './base';
/**
* The state supported by the provider (for the SAML handshake or established session).
@ -62,7 +50,7 @@ interface SAMLRequestQuery {
/**
* Defines the shape of the request with a body containing SAML response.
*/
type RequestWithSAMLPayload = Request & {
type RequestWithSAMLPayload = Legacy.Request & {
payload: { SAMLResponse: string; RelayState?: string };
};
@ -87,9 +75,11 @@ function isAccessTokenExpiredError(err?: any) {
/**
* Checks whether request payload contains SAML response from IdP.
* @param request HapiJS request instance.
* @param request Request instance.
*/
function isRequestWithSAMLResponsePayload(request: Request): request is RequestWithSAMLPayload {
function isRequestWithSAMLResponsePayload(
request: Legacy.Request
): request is RequestWithSAMLPayload {
return request.payload != null && !!(request.payload as any).SAMLResponse;
}
@ -104,19 +94,13 @@ function isSAMLRequestQuery(query: any): query is SAMLRequestQuery {
/**
* Provider that supports SAML request authentication.
*/
export class SAMLAuthenticationProvider {
/**
* Instantiates SAMLAuthenticationProvider.
* @param options Options that may be needed by authentication provider.
*/
constructor(private readonly options: ProviderOptions) {}
export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
/**
* Performs SAML request authentication.
* @param request HapiJS request instance.
* @param request Request instance.
* @param [state] Optional state object associated with the provider.
*/
public async authenticate(request: Request, state?: ProviderState | null) {
public async authenticate(request: Legacy.Request, state?: ProviderState | null) {
this.debug(`Trying to authenticate user request to ${request.url.path}.`);
let {
@ -160,10 +144,10 @@ export class SAMLAuthenticationProvider {
/**
* Invalidates SAML access token if it exists.
* @param request HapiJS request instance.
* @param request Request instance.
* @param state State value previously stored by the provider.
*/
public async deauthenticate(request: Request, state?: ProviderState) {
public async deauthenticate(request: Legacy.Request, state?: ProviderState) {
this.debug(`Trying to deauthenticate user via ${request.url.path}.`);
if ((!state || !state.accessToken) && !isSAMLRequestQuery(request.query)) {
@ -194,9 +178,9 @@ export class SAMLAuthenticationProvider {
/**
* Validates whether request contains `Bearer ***` Authorization header and just passes it
* forward to Elasticsearch backend.
* @param request HapiJS request instance.
* @param request Request instance.
*/
private async authenticateViaHeader(request: Request) {
private async authenticateViaHeader(request: Legacy.Request) {
this.debug('Trying to authenticate via header.');
const authorization = request.headers.authorization;
@ -237,7 +221,7 @@ export class SAMLAuthenticationProvider {
* When login succeeds access token is stored in the state and user is redirected to the URL
* that was requested before SAML handshake or to default Kibana location in case of IdP
* initiated login.
* @param request HapiJS request instance.
* @param request Request instance.
* @param [state] Optional state object associated with the provider.
*/
private async authenticateViaPayload(
@ -298,7 +282,7 @@ export class SAMLAuthenticationProvider {
* The tokens are stored in the state and user is redirected to the default Kibana location, unless
* we detect that user from existing session isn't the same as defined in SAML payload. In this case
* we'll forward user to a page with the respective warning.
* @param request HapiJS request instance.
* @param request Request instance.
* @param existingState State existing user session is based on.
* @param user User returned for the existing session.
*/
@ -368,10 +352,10 @@ export class SAMLAuthenticationProvider {
/**
* Tries to extract access token from state and adds it to the request before it's
* forwarded to Elasticsearch backend.
* @param request HapiJS request instance.
* @param request Request instance.
* @param state State value previously stored by the provider.
*/
private async authenticateViaState(request: Request, { accessToken }: ProviderState) {
private async authenticateViaState(request: Legacy.Request, { accessToken }: ProviderState) {
this.debug('Trying to authenticate via state.');
if (!accessToken) {
@ -404,10 +388,13 @@ export class SAMLAuthenticationProvider {
* This method is only called when authentication via access token stored in the state failed because of expired
* token. So we should use refresh token, that is also stored in the state, to extend expired access token and
* authenticate user with it.
* @param request HapiJS request instance.
* @param request Request instance.
* @param state State value previously stored by the provider.
*/
private async authenticateViaRefreshToken(request: Request, { refreshToken }: ProviderState) {
private async authenticateViaRefreshToken(
request: Legacy.Request,
{ refreshToken }: ProviderState
) {
this.debug('Trying to refresh access token.');
if (!refreshToken) {
@ -480,9 +467,9 @@ export class SAMLAuthenticationProvider {
/**
* Tries to start SAML handshake and eventually receive a token.
* @param request HapiJS request instance.
* @param request Request instance.
*/
private async authenticateViaHandshake(request: Request) {
private async authenticateViaHandshake(request: Legacy.Request) {
this.debug('Trying to initiate SAML handshake.');
// If client can't handle redirect response, we shouldn't initiate SAML handshake.
@ -577,9 +564,9 @@ export class SAMLAuthenticationProvider {
/**
* Calls `saml/invalidate` with the `SAMLRequest` query string parameter received from the Identity
* Provider and redirects user back to the Identity Provider if needed.
* @param request HapiJS request instance.
* @param request Request instance.
*/
private async performIdPInitiatedSingleLogout(request: Request) {
private async performIdPInitiatedSingleLogout(request: Legacy.Request) {
this.debug('Single logout has been initiated by the Identity Provider.');
// This operation should be performed on behalf of the user with a privilege that normal

View file

@ -8,6 +8,7 @@ import { errors } from 'elasticsearch';
import sinon from 'sinon';
import { requestFixture } from '../../__tests__/__fixtures__/request';
import { LoginAttempt } from '../login_attempt';
import { mockAuthenticationProviderOptions } from './base.mock';
import { TokenAuthenticationProvider } from './token';
describe('TokenAuthenticationProvider', () => {
@ -16,15 +17,11 @@ describe('TokenAuthenticationProvider', () => {
let callWithRequest: sinon.SinonStub;
let callWithInternalUser: sinon.SinonStub;
beforeEach(() => {
callWithRequest = sinon.stub();
callWithInternalUser = sinon.stub();
provider = new TokenAuthenticationProvider({
client: { callWithRequest, callWithInternalUser },
log() {
// no-op
},
basePath: '/base-path',
});
const providerOptions = mockAuthenticationProviderOptions();
callWithRequest = providerOptions.client.callWithRequest as sinon.SinonStub;
callWithInternalUser = providerOptions.client.callWithInternalUser as sinon.SinonStub;
provider = new TokenAuthenticationProvider(providerOptions);
});
it('does not redirect AJAX requests that can not be authenticated to the login page.', async () => {
@ -465,14 +462,10 @@ describe('TokenAuthenticationProvider', () => {
let provider: TokenAuthenticationProvider;
let callWithInternalUser: sinon.SinonStub;
beforeEach(() => {
callWithInternalUser = sinon.stub();
provider = new TokenAuthenticationProvider({
client: { callWithInternalUser } as any,
log() {
// no-op
},
basePath: '/base-path',
});
const providerOptions = mockAuthenticationProviderOptions();
callWithInternalUser = providerOptions.client.callWithInternalUser as sinon.SinonStub;
provider = new TokenAuthenticationProvider(providerOptions);
});
describe('`deauthenticate` method', () => {

View file

@ -4,22 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Request } from 'hapi';
import { Cluster } from 'src/legacy/core_plugins/elasticsearch';
import { Legacy } from 'kibana';
import { canRedirectRequest } from '../../can_redirect_request';
import { getErrorStatusCode } from '../../errors';
import { AuthenticationResult } from '../authentication_result';
import { DeauthenticationResult } from '../deauthentication_result';
import { LoginAttempt } from '../login_attempt';
/**
* Represents available provider options.
*/
interface ProviderOptions {
basePath: string;
client: Cluster;
log: (tags: string[], message: string) => void;
}
import { BaseAuthenticationProvider } from './base';
/**
* The state supported by the provider.
@ -38,7 +29,7 @@ interface ProviderState {
refreshToken?: string;
}
type RequestWithLoginAttempt = Request & {
type RequestWithLoginAttempt = Legacy.Request & {
loginAttempt: () => LoginAttempt;
};
@ -64,16 +55,10 @@ function isAccessTokenExpiredError(err?: any) {
/**
* Provider that supports token-based request authentication.
*/
export class TokenAuthenticationProvider {
/**
* Instantiates TokenAuthenticationProvider.
* @param options Options that may be needed by authentication provider.
*/
constructor(private readonly options: ProviderOptions) {}
export class TokenAuthenticationProvider extends BaseAuthenticationProvider {
/**
* Performs token-based request authentication
* @param request HapiJS request instance.
* @param request Request instance.
* @param [state] Optional state object associated with the provider.
*/
public async authenticate(request: RequestWithLoginAttempt, state?: ProviderState | null) {
@ -113,10 +98,10 @@ export class TokenAuthenticationProvider {
/**
* Redirects user to the login page preserving query string parameters.
* @param request HapiJS request instance.
* @param request Request instance.
* @param state State value previously stored by the provider.
*/
public async deauthenticate(request: Request, state?: ProviderState | null) {
public async deauthenticate(request: Legacy.Request, state?: ProviderState | null) {
this.debug(`Trying to deauthenticate user via ${request.url.path}.`);
if (!state || !state.accessToken || !state.refreshToken) {
@ -173,9 +158,9 @@ export class TokenAuthenticationProvider {
/**
* Validates whether request contains `Bearer ***` Authorization header and just passes it
* forward to Elasticsearch backend.
* @param request HapiJS request instance.
* @param request Request instance.
*/
private async authenticateViaHeader(request: Request) {
private async authenticateViaHeader(request: Legacy.Request) {
this.debug('Trying to authenticate via header.');
const authorization = request.headers.authorization;
@ -207,7 +192,7 @@ export class TokenAuthenticationProvider {
/**
* Validates whether request contains a login payload and authenticates the
* user if necessary.
* @param request HapiJS request instance.
* @param request Request instance.
*/
private async authenticateViaLoginAttempt(request: RequestWithLoginAttempt) {
this.debug('Trying to authenticate via login attempt.');
@ -264,10 +249,10 @@ export class TokenAuthenticationProvider {
/**
* Tries to extract authorization header from the state and adds it to the request before
* it's forwarded to Elasticsearch backend.
* @param request HapiJS request instance.
* @param request Request instance.
* @param state State value previously stored by the provider.
*/
private async authenticateViaState(request: Request, { accessToken }: ProviderState) {
private async authenticateViaState(request: Legacy.Request, { accessToken }: ProviderState) {
this.debug('Trying to authenticate via state.');
if (!accessToken) {
@ -300,10 +285,13 @@ export class TokenAuthenticationProvider {
* This method is only called when authentication via access token stored in the state failed because of expired
* token. So we should use refresh token, that is also stored in the state, to extend expired access token and
* authenticate user with it.
* @param request HapiJS request instance.
* @param request Request instance.
* @param state State value previously stored by the provider.
*/
private async authenticateViaRefreshToken(request: Request, { refreshToken }: ProviderState) {
private async authenticateViaRefreshToken(
request: Legacy.Request,
{ refreshToken }: ProviderState
) {
this.debug('Trying to refresh access token.');
if (!refreshToken) {
@ -367,9 +355,9 @@ export class TokenAuthenticationProvider {
/**
* Constructs login page URL using current url path as `next` query string parameter.
* @param request HapiJS request instance.
* @param request Request instance.
*/
private getLoginPageURL(request: Request) {
private getLoginPageURL(request: Legacy.Request) {
const nextURL = encodeURIComponent(`${request.getBasePath()}${request.url.path}`);
return `${this.options.basePath}/login?next=${nextURL}`;
}

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Request } from 'hapi';
import hapiAuthCookie from 'hapi-auth-cookie';
import { Legacy } from 'kibana';
@ -29,7 +28,7 @@ interface InternalSession {
expires: number | null;
}
function assertRequest(request: Request) {
function assertRequest(request: Legacy.Request) {
if (!request || typeof request !== 'object') {
throw new Error(`Request should be a valid object, was [${typeof request}].`);
}
@ -47,7 +46,7 @@ export class Session {
/**
* Instantiates Session. Constructor is not supposed to be used directly. To make sure that all
* `Session` dependencies/plugins are properly initialized one should use static `Session.create` instead.
* @param server HapiJS Server instance.
* @param server Server instance.
*/
constructor(private readonly server: Legacy.Server) {
this.ttl = this.server.config().get<number | null>('xpack.security.sessionTimeout');
@ -55,9 +54,9 @@ export class Session {
/**
* Retrieves session value from the session storage (e.g. cookie).
* @param request HapiJS request instance.
* @param request Request instance.
*/
async get(request: Request) {
async get<T>(request: Legacy.Request) {
assertRequest(request);
try {
@ -65,12 +64,12 @@ export class Session {
// If it's not an array, just return the session value
if (!Array.isArray(session)) {
return session.value;
return session.value as T;
}
// If we have an array with one value, we're good also
if (session.length === 1) {
return session[0].value;
return session[0].value as T;
}
// Otherwise, we have more than one and won't be authing the user because we don't
@ -88,10 +87,10 @@ export class Session {
/**
* Puts current session value into the session storage.
* @param request HapiJS request instance.
* @param request Request instance.
* @param value Any object that will be associated with the request.
*/
async set(request: Request, value: unknown) {
async set(request: Legacy.Request, value: unknown) {
assertRequest(request);
request.cookieAuth.set({
@ -102,9 +101,9 @@ export class Session {
/**
* Clears current session.
* @param request HapiJS request instance.
* @param request Request instance.
*/
async clear(request: Request) {
async clear(request: Legacy.Request) {
assertRequest(request);
request.cookieAuth.clear();
@ -112,7 +111,7 @@ export class Session {
/**
* Prepares and creates a session instance.
* @param server HapiJS Server instance.
* @param server Server instance.
*/
static async create(server: Legacy.Server) {
// Register HAPI plugin that manages session cookie and delegate parsing of the session cookie to it.
@ -151,10 +150,10 @@ export class Session {
/**
* Validation function that is passed to hapi-auth-cookie plugin and is responsible
* only for cookie expiration time validation.
* @param request HapiJS request instance.
* @param request Request instance.
* @param session Session value object retrieved from cookie.
*/
private static validateCookie(request: Request, session: InternalSession) {
private static validateCookie(request: Legacy.Request, session: InternalSession) {
if (session.expires && session.expires < Date.now()) {
return { valid: false };
}

View file

@ -5,7 +5,6 @@
*/
export { Actions } from './actions';
// @ts-ignore
export { createAuthorizationService } from './service';
export { disableUICapabilitesFactory } from './disable_ui_capabilities';
export { initAPIAuthorization } from './api_authorization';

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
// @ts-ignore
import { requestFixture } from '../__tests__/__fixtures__/request';
import { authorizationModeFactory } from './mode';

View file

@ -13,7 +13,6 @@ import {
mockPrivilegesFactory,
} from './service.test.mocks';
// @ts-ignore
import { getClient } from '../../../../../server/lib/get_client_shield';
import { actionsFactory } from './actions';
import { checkPrivilegesWithRequestFactory } from './check_privileges';

View file

@ -6,7 +6,6 @@
import { Server } from 'hapi';
// @ts-ignore
import { getClient } from '../../../../../server/lib/get_client_shield';
import { SpacesPlugin } from '../../../../spaces/types';
import { XPackFeature, XPackMainPlugin } from '../../../../xpack_main/xpack_main';

View file

@ -4,14 +4,13 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { Request } from 'hapi';
// @ts-ignore
import { Legacy } from 'kibana';
import { getClient } from '../../../../server/lib/get_client_shield';
export function getUserProvider(server: any) {
const callWithRequest = getClient(server).callWithRequest;
server.expose('getUser', async (request: Request) => {
server.expose('getUser', async (request: Legacy.Request) => {
const xpackInfo = server.plugins.xpack_main.info;
if (xpackInfo && xpackInfo.isAvailable() && !xpackInfo.feature('security').isEnabled()) {
return Promise.resolve(null);

View file

@ -4,24 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import Boom from 'boom';
import { PluginProperties, Server } from 'hapi';
import { Server } from 'hapi';
import { RawKibanaPrivileges } from '../../../../../common/model';
import { initGetPrivilegesApi } from './get';
interface KibanaPluginProperties extends PluginProperties {
security: {
authorization: {
privileges: {
get: () => RawKibanaPrivileges;
};
};
};
}
interface KibanaServer extends Server {
plugins: KibanaPluginProperties;
}
const createRawKibanaPrivileges: () => RawKibanaPrivileges = () => {
return {
features: {
@ -48,7 +34,7 @@ const createRawKibanaPrivileges: () => RawKibanaPrivileges = () => {
};
const createMockServer = () => {
const mockServer: KibanaServer = new Server({ debug: false, port: 8080 }) as KibanaServer;
const mockServer = new Server({ debug: false, port: 8080 });
mockServer.plugins.security = {
authorization: {
@ -58,7 +44,7 @@ const createMockServer = () => {
}),
},
},
};
} as any;
return mockServer;
};

View file

@ -8,14 +8,13 @@ jest.mock('../../../../server/lib/get_client_shield', () => ({
}));
import Boom from 'boom';
// @ts-ignore
import { getClient } from '../../../../server/lib/get_client_shield';
import { createDefaultSpace } from './create_default_space';
let mockCallWithRequest;
beforeEach(() => {
mockCallWithRequest = jest.fn();
getClient.mockReturnValue({
(getClient as jest.Mock).mockReturnValue({
callWithRequest: mockCallWithRequest,
});
});

View file

@ -5,7 +5,6 @@
*/
import { i18n } from '@kbn/i18n';
// @ts-ignore
import { getClient } from '../../../../server/lib/get_client_shield';
import { DEFAULT_SPACE_ID } from '../../common/constants';

View file

@ -5,8 +5,10 @@
*/
import { once } from 'lodash';
import { Legacy } from 'kibana';
// @ts-ignore
import esShield from './esjs_shield_plugin';
export const getClient = once((server) => {
export const getClient = once((server: Legacy.Server) => {
return server.plugins.elasticsearch.createCluster('security', { plugins: [esShield] });
});

View file

@ -7,11 +7,13 @@
import 'hapi';
import { CloudPlugin } from '../plugins/cloud';
import { SecurityPlugin } from '../plugins/security';
import { XPackMainPlugin } from '../plugins/xpack_main/xpack_main';
declare module 'hapi' {
interface PluginProperties {
cloud?: CloudPlugin;
xpack_main: XPackMainPlugin;
security?: SecurityPlugin;
}
}