mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
* check if a provider is there * the last puzzle * fix lint * review oleg * Update x-pack/plugins/security/public/nav_control/nav_control_component.tsx Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com> * Update x-pack/test/security_api_integration/http_no_auth_providers.config.ts Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com> * Update x-pack/plugins/security/server/authentication/authenticator.test.ts Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com> * Update x-pack/test/security_api_integration/tests/http_no_auth_providers/authentication.ts Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com> * Update x-pack/test/security_api_integration/http_no_auth_providers.config.ts Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com> * review II Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>
2050 lines
80 KiB
TypeScript
2050 lines
80 KiB
TypeScript
/*
|
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
* or more contributor license agreements. Licensed under the Elastic License
|
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
* 2.0.
|
|
*/
|
|
|
|
jest.mock('./providers/basic');
|
|
jest.mock('./providers/token');
|
|
jest.mock('./providers/saml');
|
|
jest.mock('./providers/http');
|
|
|
|
import Boom from '@hapi/boom';
|
|
|
|
import type { PublicMethodsOf } from '@kbn/utility-types';
|
|
import {
|
|
elasticsearchServiceMock,
|
|
httpServerMock,
|
|
httpServiceMock,
|
|
loggingSystemMock,
|
|
} from 'src/core/server/mocks';
|
|
|
|
import {
|
|
AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER,
|
|
AUTH_URL_HASH_QUERY_STRING_PARAMETER,
|
|
} from '../../common/constants';
|
|
import type { SecurityLicenseFeatures } from '../../common/licensing';
|
|
import { licenseMock } from '../../common/licensing/index.mock';
|
|
import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock';
|
|
import { auditServiceMock } from '../audit/index.mock';
|
|
import { ConfigSchema, createConfig } from '../config';
|
|
import { securityFeatureUsageServiceMock } from '../feature_usage/index.mock';
|
|
import type { SessionValue } from '../session_management';
|
|
import { sessionMock } from '../session_management/index.mock';
|
|
import { AuthenticationResult } from './authentication_result';
|
|
import type { AuthenticatorOptions } from './authenticator';
|
|
import { Authenticator } from './authenticator';
|
|
import { DeauthenticationResult } from './deauthentication_result';
|
|
import type { BasicAuthenticationProvider, SAMLAuthenticationProvider } from './providers';
|
|
|
|
function getMockOptions({
|
|
providers,
|
|
http = {},
|
|
selector,
|
|
}: {
|
|
providers?: Record<string, unknown> | string[];
|
|
http?: Partial<AuthenticatorOptions['config']['authc']['http']>;
|
|
selector?: AuthenticatorOptions['config']['authc']['selector'];
|
|
} = {}) {
|
|
return {
|
|
audit: auditServiceMock.create(),
|
|
getCurrentUser: jest.fn(),
|
|
clusterClient: elasticsearchServiceMock.createClusterClient(),
|
|
basePath: httpServiceMock.createSetupContract().basePath,
|
|
license: licenseMock.create(),
|
|
loggers: loggingSystemMock.create(),
|
|
getServerBaseURL: jest.fn(),
|
|
config: createConfig(
|
|
ConfigSchema.validate({ authc: { selector, providers, http } }),
|
|
loggingSystemMock.create().get(),
|
|
{ isTLSEnabled: false }
|
|
),
|
|
session: sessionMock.create(),
|
|
featureUsageService: securityFeatureUsageServiceMock.createStartContract(),
|
|
};
|
|
}
|
|
|
|
describe('Authenticator', () => {
|
|
let mockBasicAuthenticationProvider: jest.Mocked<PublicMethodsOf<BasicAuthenticationProvider>>;
|
|
beforeEach(() => {
|
|
mockBasicAuthenticationProvider = {
|
|
login: jest.fn(),
|
|
authenticate: jest.fn().mockResolvedValue(AuthenticationResult.notHandled()),
|
|
logout: jest.fn().mockResolvedValue(DeauthenticationResult.notHandled()),
|
|
getHTTPAuthenticationScheme: jest.fn(),
|
|
};
|
|
|
|
jest.requireMock('./providers/http').HTTPAuthenticationProvider.mockImplementation(() => ({
|
|
type: 'http',
|
|
authenticate: jest.fn().mockResolvedValue(AuthenticationResult.notHandled()),
|
|
logout: jest.fn().mockResolvedValue(DeauthenticationResult.notHandled()),
|
|
}));
|
|
|
|
jest.requireMock('./providers/basic').BasicAuthenticationProvider.mockImplementation(() => ({
|
|
type: 'basic',
|
|
...mockBasicAuthenticationProvider,
|
|
}));
|
|
|
|
jest.requireMock('./providers/saml').SAMLAuthenticationProvider.mockImplementation(() => ({
|
|
type: 'saml',
|
|
authenticate: jest.fn().mockResolvedValue(AuthenticationResult.notHandled()),
|
|
getHTTPAuthenticationScheme: jest.fn(),
|
|
}));
|
|
});
|
|
|
|
afterEach(() => jest.clearAllMocks());
|
|
|
|
describe('initialization', () => {
|
|
it('fails if authentication providers are not configured.', () => {
|
|
expect(
|
|
() => new Authenticator(getMockOptions({ providers: {}, http: { enabled: false } }))
|
|
).toThrowError(
|
|
'No authentication provider is configured. Verify `xpack.security.authc.*` config value.'
|
|
);
|
|
});
|
|
|
|
it('fails if configured authentication provider is not known.', () => {
|
|
expect(() => new Authenticator(getMockOptions({ providers: ['super-basic'] }))).toThrowError(
|
|
'Unsupported authentication provider name: super-basic.'
|
|
);
|
|
});
|
|
|
|
it('fails if any of the user specified provider uses reserved __http__ name.', () => {
|
|
expect(
|
|
() =>
|
|
new Authenticator(getMockOptions({ providers: { basic: { __http__: { order: 0 } } } }))
|
|
).toThrowError('Provider name "__http__" is reserved.');
|
|
});
|
|
|
|
describe('#options.urls.loggedOut', () => {
|
|
it('points to /login if provider requires login form', () => {
|
|
const authenticationProviderMock =
|
|
jest.requireMock(`./providers/basic`).BasicAuthenticationProvider;
|
|
authenticationProviderMock.mockClear();
|
|
new Authenticator(getMockOptions());
|
|
const getLoggedOutURL = authenticationProviderMock.mock.calls[0][0].urls.loggedOut;
|
|
|
|
expect(getLoggedOutURL(httpServerMock.createKibanaRequest())).toBe(
|
|
'/mock-server-basepath/login?msg=LOGGED_OUT'
|
|
);
|
|
|
|
expect(
|
|
getLoggedOutURL(
|
|
httpServerMock.createKibanaRequest({
|
|
query: { next: '/app/ml/encode me', msg: 'SESSION_EXPIRED' },
|
|
})
|
|
)
|
|
).toBe('/mock-server-basepath/login?next=%2Fapp%2Fml%2Fencode+me&msg=SESSION_EXPIRED');
|
|
});
|
|
|
|
it('points to /login if login selector is enabled', () => {
|
|
const authenticationProviderMock =
|
|
jest.requireMock(`./providers/saml`).SAMLAuthenticationProvider;
|
|
authenticationProviderMock.mockClear();
|
|
new Authenticator(
|
|
getMockOptions({
|
|
selector: { enabled: true },
|
|
providers: { saml: { saml1: { order: 0, realm: 'realm' } } },
|
|
})
|
|
);
|
|
const getLoggedOutURL = authenticationProviderMock.mock.calls[0][0].urls.loggedOut;
|
|
|
|
expect(getLoggedOutURL(httpServerMock.createKibanaRequest())).toBe(
|
|
'/mock-server-basepath/login?msg=LOGGED_OUT'
|
|
);
|
|
|
|
expect(
|
|
getLoggedOutURL(
|
|
httpServerMock.createKibanaRequest({
|
|
query: { next: '/app/ml/encode me', msg: 'SESSION_EXPIRED' },
|
|
})
|
|
)
|
|
).toBe('/mock-server-basepath/login?next=%2Fapp%2Fml%2Fencode+me&msg=SESSION_EXPIRED');
|
|
});
|
|
|
|
it('points to /security/logged_out if login selector is NOT enabled', () => {
|
|
const authenticationProviderMock =
|
|
jest.requireMock(`./providers/saml`).SAMLAuthenticationProvider;
|
|
authenticationProviderMock.mockClear();
|
|
new Authenticator(
|
|
getMockOptions({
|
|
selector: { enabled: false },
|
|
providers: { saml: { saml1: { order: 0, realm: 'realm' } } },
|
|
})
|
|
);
|
|
const getLoggedOutURL = authenticationProviderMock.mock.calls[0][0].urls.loggedOut;
|
|
|
|
expect(getLoggedOutURL(httpServerMock.createKibanaRequest())).toBe(
|
|
'/mock-server-basepath/security/logged_out?msg=LOGGED_OUT'
|
|
);
|
|
|
|
expect(
|
|
getLoggedOutURL(
|
|
httpServerMock.createKibanaRequest({
|
|
query: { next: '/app/ml/encode me', msg: 'SESSION_EXPIRED' },
|
|
})
|
|
)
|
|
).toBe(
|
|
'/mock-server-basepath/security/logged_out?next=%2Fapp%2Fml%2Fencode+me&msg=SESSION_EXPIRED'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('HTTP authentication provider', () => {
|
|
beforeEach(() => {
|
|
jest
|
|
.requireMock('./providers/basic')
|
|
.BasicAuthenticationProvider.mockImplementation(() => ({
|
|
type: 'basic',
|
|
getHTTPAuthenticationScheme: jest.fn().mockReturnValue('basic'),
|
|
}));
|
|
});
|
|
|
|
afterEach(() => jest.resetAllMocks());
|
|
|
|
it('enabled by default', () => {
|
|
new Authenticator(getMockOptions());
|
|
|
|
expect(
|
|
jest.requireMock('./providers/http').HTTPAuthenticationProvider
|
|
).toHaveBeenCalledWith(expect.anything(), {
|
|
supportedSchemes: new Set(['apikey', 'bearer', 'basic']),
|
|
});
|
|
});
|
|
|
|
it('includes all required schemes if `autoSchemesEnabled` is enabled', () => {
|
|
new Authenticator(
|
|
getMockOptions({
|
|
providers: { basic: { basic1: { order: 0 } }, kerberos: { kerberos1: { order: 1 } } },
|
|
})
|
|
);
|
|
|
|
expect(
|
|
jest.requireMock('./providers/http').HTTPAuthenticationProvider
|
|
).toHaveBeenCalledWith(expect.anything(), {
|
|
supportedSchemes: new Set(['apikey', 'basic', 'bearer']),
|
|
});
|
|
});
|
|
|
|
it('does not include additional schemes if `autoSchemesEnabled` is disabled', () => {
|
|
new Authenticator(
|
|
getMockOptions({
|
|
providers: { basic: { basic1: { order: 0 } }, kerberos: { kerberos1: { order: 1 } } },
|
|
http: { autoSchemesEnabled: false },
|
|
})
|
|
);
|
|
|
|
expect(
|
|
jest.requireMock('./providers/http').HTTPAuthenticationProvider
|
|
).toHaveBeenCalledWith(expect.anything(), {
|
|
supportedSchemes: new Set(['apikey', 'bearer']),
|
|
});
|
|
});
|
|
|
|
it('disabled if explicitly disabled', () => {
|
|
new Authenticator(
|
|
getMockOptions({
|
|
providers: { basic: { basic1: { order: 0 } } },
|
|
http: { enabled: false },
|
|
})
|
|
);
|
|
|
|
expect(
|
|
jest.requireMock('./providers/http').HTTPAuthenticationProvider
|
|
).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('`login` method', () => {
|
|
let authenticator: Authenticator;
|
|
let mockOptions: ReturnType<typeof getMockOptions>;
|
|
let mockSessVal: SessionValue;
|
|
const auditLogger = {
|
|
log: jest.fn(),
|
|
};
|
|
|
|
beforeEach(() => {
|
|
auditLogger.log.mockClear();
|
|
mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } });
|
|
mockOptions.session.get.mockResolvedValue(null);
|
|
mockOptions.audit.asScoped.mockReturnValue(auditLogger);
|
|
mockSessVal = sessionMock.createValue({ state: { authorization: 'Basic xxx' } });
|
|
|
|
authenticator = new Authenticator(mockOptions);
|
|
});
|
|
|
|
it('fails if request is not provided.', async () => {
|
|
await expect(authenticator.login(undefined as any, undefined as any)).rejects.toThrowError(
|
|
'Request should be a valid "KibanaRequest" instance, was [undefined].'
|
|
);
|
|
});
|
|
|
|
it('fails if login attempt is not provided or invalid.', async () => {
|
|
await expect(
|
|
authenticator.login(httpServerMock.createKibanaRequest(), undefined as any)
|
|
).rejects.toThrowError(
|
|
'Login attempt should be an object with non-empty "provider.type" or "provider.name" property.'
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(httpServerMock.createKibanaRequest(), {} as any)
|
|
).rejects.toThrowError(
|
|
'Login attempt should be an object with non-empty "provider.type" or "provider.name" property.'
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(httpServerMock.createKibanaRequest(), {
|
|
provider: 'basic',
|
|
value: {},
|
|
} as any)
|
|
).rejects.toThrowError(
|
|
'Login attempt should be an object with non-empty "provider.type" or "provider.name" property.'
|
|
);
|
|
});
|
|
|
|
it('fails if an authentication provider fails.', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
const failureReason = new Error('Not Authorized');
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.failed(failureReason)
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(AuthenticationResult.failed(failureReason));
|
|
});
|
|
|
|
it('returns user that authentication provider returns.', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
|
|
const user = mockAuthenticatedUser();
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } })
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(
|
|
AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } })
|
|
);
|
|
});
|
|
|
|
it('adds audit event when successful.', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
const user = mockAuthenticatedUser();
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } })
|
|
);
|
|
await authenticator.login(request, { provider: { type: 'basic' }, value: {} });
|
|
|
|
expect(auditLogger.log).toHaveBeenCalledTimes(1);
|
|
expect(auditLogger.log).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
event: { action: 'user_login', category: ['authentication'], outcome: 'success' },
|
|
})
|
|
);
|
|
});
|
|
|
|
it('adds audit event when not successful.', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
const failureReason = new Error('Not Authorized');
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.failed(failureReason)
|
|
);
|
|
await authenticator.login(request, { provider: { type: 'basic' }, value: {} });
|
|
|
|
expect(auditLogger.log).toHaveBeenCalledTimes(1);
|
|
expect(auditLogger.log).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
event: { action: 'user_login', category: ['authentication'], outcome: 'failure' },
|
|
})
|
|
);
|
|
});
|
|
|
|
it('does not add audit event when not handled.', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'token' }, value: {} })
|
|
).resolves.toEqual(AuthenticationResult.notHandled());
|
|
|
|
await authenticator.login(request, { provider: { name: 'basic2' }, value: {} });
|
|
|
|
expect(auditLogger.log).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('creates session whenever authentication provider returns state', async () => {
|
|
const user = mockAuthenticatedUser();
|
|
const request = httpServerMock.createKibanaRequest();
|
|
const authorization = `Basic ${Buffer.from('foo:bar').toString('base64')}`;
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(user, { state: { authorization } })
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(AuthenticationResult.succeeded(user, { state: { authorization } }));
|
|
|
|
expect(mockOptions.session.create).toHaveBeenCalledTimes(1);
|
|
expect(mockOptions.session.create).toHaveBeenCalledWith(request, {
|
|
username: user.username,
|
|
provider: mockSessVal.provider,
|
|
state: { authorization },
|
|
});
|
|
});
|
|
|
|
it('returns `notHandled` if login attempt is targeted to not configured provider.', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'token' }, value: {} })
|
|
).resolves.toEqual(AuthenticationResult.notHandled());
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { name: 'basic2' }, value: {} })
|
|
).resolves.toEqual(AuthenticationResult.notHandled());
|
|
});
|
|
|
|
describe('multi-provider scenarios', () => {
|
|
let mockSAMLAuthenticationProvider1: jest.Mocked<PublicMethodsOf<SAMLAuthenticationProvider>>;
|
|
let mockSAMLAuthenticationProvider2: jest.Mocked<PublicMethodsOf<SAMLAuthenticationProvider>>;
|
|
|
|
beforeEach(() => {
|
|
mockSAMLAuthenticationProvider1 = {
|
|
login: jest.fn().mockResolvedValue(AuthenticationResult.notHandled()),
|
|
authenticate: jest.fn(),
|
|
logout: jest.fn(),
|
|
getHTTPAuthenticationScheme: jest.fn(),
|
|
};
|
|
|
|
mockSAMLAuthenticationProvider2 = {
|
|
login: jest.fn().mockResolvedValue(AuthenticationResult.notHandled()),
|
|
authenticate: jest.fn(),
|
|
logout: jest.fn(),
|
|
getHTTPAuthenticationScheme: jest.fn(),
|
|
};
|
|
|
|
jest
|
|
.requireMock('./providers/saml')
|
|
.SAMLAuthenticationProvider.mockImplementationOnce(() => ({
|
|
type: 'saml',
|
|
...mockSAMLAuthenticationProvider1,
|
|
}))
|
|
.mockImplementationOnce(() => ({
|
|
type: 'saml',
|
|
...mockSAMLAuthenticationProvider2,
|
|
}));
|
|
|
|
mockOptions = getMockOptions({
|
|
providers: {
|
|
basic: { basic1: { order: 0 } },
|
|
saml: {
|
|
saml1: { realm: 'saml1-realm', order: 1 },
|
|
saml2: { realm: 'saml2-realm', order: 2 },
|
|
},
|
|
},
|
|
});
|
|
mockOptions.session.get.mockResolvedValue(null);
|
|
|
|
authenticator = new Authenticator(mockOptions);
|
|
});
|
|
|
|
it('tries to login only with the provider that has specified name', async () => {
|
|
const user = mockAuthenticatedUser();
|
|
const request = httpServerMock.createKibanaRequest();
|
|
|
|
mockSAMLAuthenticationProvider2.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(user, { state: { token: 'access-token' } })
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { name: 'saml2' }, value: {} })
|
|
).resolves.toEqual(
|
|
AuthenticationResult.succeeded(user, { state: { token: 'access-token' } })
|
|
);
|
|
|
|
expect(mockOptions.session.create).toHaveBeenCalledTimes(1);
|
|
expect(mockOptions.session.create).toHaveBeenCalledWith(request, {
|
|
username: user.username,
|
|
provider: { type: 'saml', name: 'saml2' },
|
|
state: { token: 'access-token' },
|
|
});
|
|
|
|
expect(mockBasicAuthenticationProvider.login).not.toHaveBeenCalled();
|
|
expect(mockSAMLAuthenticationProvider1.login).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('tries to login only with the provider that has specified type', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'saml' }, value: {} })
|
|
).resolves.toEqual(AuthenticationResult.notHandled());
|
|
|
|
expect(mockOptions.session.create).not.toHaveBeenCalled();
|
|
|
|
expect(mockBasicAuthenticationProvider.login).not.toHaveBeenCalled();
|
|
expect(mockSAMLAuthenticationProvider1.login).toHaveBeenCalledTimes(1);
|
|
expect(mockSAMLAuthenticationProvider2.login).toHaveBeenCalledTimes(1);
|
|
expect(mockSAMLAuthenticationProvider1.login.mock.invocationCallOrder[0]).toBeLessThan(
|
|
mockSAMLAuthenticationProvider2.login.mock.invocationCallOrder[0]
|
|
);
|
|
});
|
|
|
|
it('returns as soon as provider handles request', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
const user = mockAuthenticatedUser();
|
|
|
|
const authenticationResults = [
|
|
AuthenticationResult.failed(new Error('Fail')),
|
|
AuthenticationResult.succeeded(user, { state: { result: '200' } }),
|
|
AuthenticationResult.redirectTo('/some/url', { state: { result: '302' } }),
|
|
];
|
|
|
|
for (const result of authenticationResults) {
|
|
mockSAMLAuthenticationProvider1.login.mockResolvedValue(result);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'saml' }, value: {} })
|
|
).resolves.toEqual(result);
|
|
}
|
|
|
|
expect(mockOptions.session.create).toHaveBeenCalledTimes(2);
|
|
expect(mockOptions.session.create).toHaveBeenCalledWith(request, {
|
|
username: user.username,
|
|
provider: { type: 'saml', name: 'saml1' },
|
|
state: { result: '200' },
|
|
});
|
|
expect(mockOptions.session.create).toHaveBeenCalledWith(request, {
|
|
username: undefined,
|
|
provider: { type: 'saml', name: 'saml1' },
|
|
state: { result: '302' },
|
|
});
|
|
|
|
expect(mockBasicAuthenticationProvider.login).not.toHaveBeenCalled();
|
|
expect(mockSAMLAuthenticationProvider2.login).not.toHaveBeenCalled();
|
|
expect(mockSAMLAuthenticationProvider1.login).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it('provides session only if provider name matches', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
|
|
mockOptions.session.get.mockResolvedValue({
|
|
...mockSessVal,
|
|
provider: { type: 'saml', name: 'saml2' },
|
|
});
|
|
|
|
const loginAttemptValue = Symbol('attempt');
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'saml' }, value: loginAttemptValue })
|
|
).resolves.toEqual(AuthenticationResult.notHandled());
|
|
|
|
expect(mockBasicAuthenticationProvider.login).not.toHaveBeenCalled();
|
|
|
|
expect(mockSAMLAuthenticationProvider1.login).toHaveBeenCalledTimes(1);
|
|
expect(mockSAMLAuthenticationProvider1.login).toHaveBeenCalledWith(
|
|
request,
|
|
loginAttemptValue,
|
|
null
|
|
);
|
|
|
|
expect(mockSAMLAuthenticationProvider2.login).toHaveBeenCalledTimes(1);
|
|
expect(mockSAMLAuthenticationProvider2.login).toHaveBeenCalledWith(
|
|
request,
|
|
loginAttemptValue,
|
|
mockSessVal.state
|
|
);
|
|
|
|
// Presence of the session has precedence over order.
|
|
expect(mockSAMLAuthenticationProvider2.login.mock.invocationCallOrder[0]).toBeLessThan(
|
|
mockSAMLAuthenticationProvider1.login.mock.invocationCallOrder[0]
|
|
);
|
|
});
|
|
});
|
|
|
|
it('clears session if it belongs to a not configured provider or with the name that is registered but has different type.', async () => {
|
|
const user = mockAuthenticatedUser();
|
|
const credentials = { username: 'user', password: 'password' };
|
|
const request = httpServerMock.createKibanaRequest();
|
|
|
|
// Re-configure authenticator with `token` provider that uses the name of `basic`.
|
|
const loginMock = jest.fn().mockResolvedValue(AuthenticationResult.succeeded(user));
|
|
jest.requireMock('./providers/token').TokenAuthenticationProvider.mockImplementation(() => ({
|
|
type: 'token',
|
|
login: loginMock,
|
|
getHTTPAuthenticationScheme: jest.fn(),
|
|
}));
|
|
mockOptions = getMockOptions({ providers: { token: { basic1: { order: 0 } } } });
|
|
authenticator = new Authenticator(mockOptions);
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(AuthenticationResult.succeeded(user));
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { name: 'basic1' }, value: credentials })
|
|
).resolves.toEqual(AuthenticationResult.succeeded(user));
|
|
|
|
expect(loginMock).toHaveBeenCalledWith(request, credentials, null);
|
|
|
|
expect(mockOptions.session.create).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.update).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.extend).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1);
|
|
expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' });
|
|
});
|
|
|
|
it('clears session if provider asked to do so in `succeeded` result.', async () => {
|
|
const user = mockAuthenticatedUser();
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(user, { state: null })
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(AuthenticationResult.succeeded(user, { state: null }));
|
|
|
|
expect(mockOptions.session.create).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.update).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.extend).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1);
|
|
expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' });
|
|
});
|
|
|
|
it('clears session if provider asked to do so in `redirected` result.', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.redirectTo('some-url', { state: null })
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(AuthenticationResult.redirectTo('some-url', { state: null }));
|
|
|
|
expect(mockOptions.session.create).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.update).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.extend).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1);
|
|
expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' });
|
|
});
|
|
|
|
describe('with Access Agreement', () => {
|
|
const mockUser = mockAuthenticatedUser();
|
|
beforeEach(() => {
|
|
mockOptions = getMockOptions({
|
|
providers: {
|
|
basic: { basic1: { order: 0, accessAgreement: { message: 'some notice' } } },
|
|
},
|
|
});
|
|
|
|
mockOptions.session.update.mockImplementation(async (request, value) => value);
|
|
mockOptions.session.extend.mockImplementation(async (request, value) => value);
|
|
mockOptions.session.create.mockImplementation(async (request, value) => ({
|
|
...mockSessVal,
|
|
...value,
|
|
}));
|
|
|
|
mockOptions.license.getFeatures.mockReturnValue({
|
|
allowAccessAgreement: true,
|
|
} as SecurityLicenseFeatures);
|
|
|
|
authenticator = new Authenticator(mockOptions);
|
|
});
|
|
|
|
it('does not redirect to Access Agreement if authenticated session is not created', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue(null);
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser)
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(AuthenticationResult.succeeded(mockUser));
|
|
});
|
|
|
|
it('does not redirect to Access Agreement if request cannot be handled', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(AuthenticationResult.notHandled());
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(AuthenticationResult.notHandled());
|
|
});
|
|
|
|
it('does not redirect to Access Agreement if authentication fails', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
const failureReason = new Error('something went wrong');
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.failed(failureReason)
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(AuthenticationResult.failed(failureReason));
|
|
});
|
|
|
|
it('does not redirect to Access Agreement if redirect is required to complete login', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue(null);
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.redirectTo('/some-url', { state: 'some-state' })
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(AuthenticationResult.redirectTo('/some-url', { state: 'some-state' }));
|
|
});
|
|
|
|
it('does not redirect to Access Agreement if user has already acknowledged it', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue({
|
|
...mockSessVal,
|
|
accessAgreementAcknowledged: true,
|
|
});
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser, { state: 'some-state' })
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(AuthenticationResult.succeeded(mockUser, { state: 'some-state' }));
|
|
});
|
|
|
|
it('does not redirect to Access Agreement its own requests', async () => {
|
|
const request = httpServerMock.createKibanaRequest({ path: '/security/access_agreement' });
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser, { state: 'some-state' })
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(AuthenticationResult.succeeded(mockUser, { state: 'some-state' }));
|
|
});
|
|
|
|
it('does not redirect to Access Agreement if it is not configured', async () => {
|
|
mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } });
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
authenticator = new Authenticator(mockOptions);
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser, { state: 'some-state' })
|
|
);
|
|
|
|
const request = httpServerMock.createKibanaRequest();
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(AuthenticationResult.succeeded(mockUser, { state: 'some-state' }));
|
|
});
|
|
|
|
it('does not redirect to Access Agreement if license doesnt allow it.', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
mockOptions.license.getFeatures.mockReturnValue({
|
|
allowAccessAgreement: false,
|
|
} as SecurityLicenseFeatures);
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser, { state: 'some-state' })
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(AuthenticationResult.succeeded(mockUser, { state: 'some-state' }));
|
|
});
|
|
|
|
it('redirects to Access Agreement when needed.', async () => {
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser, {
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
|
|
const request = httpServerMock.createKibanaRequest();
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(
|
|
AuthenticationResult.redirectTo(
|
|
'/mock-server-basepath/security/access_agreement?next=%2Fmock-server-basepath%2Fpath',
|
|
{
|
|
user: mockUser,
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
}
|
|
)
|
|
);
|
|
});
|
|
|
|
it('redirects to Access Agreement preserving redirect URL specified in login attempt.', async () => {
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser, {
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
|
|
const request = httpServerMock.createKibanaRequest();
|
|
await expect(
|
|
authenticator.login(request, {
|
|
provider: { type: 'basic' },
|
|
value: {},
|
|
redirectURL: '/some-url',
|
|
})
|
|
).resolves.toEqual(
|
|
AuthenticationResult.redirectTo(
|
|
'/mock-server-basepath/security/access_agreement?next=%2Fsome-url',
|
|
{
|
|
user: mockUser,
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
}
|
|
)
|
|
);
|
|
});
|
|
|
|
it('redirects to Access Agreement preserving redirect URL specified in the authentication result.', async () => {
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.redirectTo('/some-url', {
|
|
user: mockUser,
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
|
|
const request = httpServerMock.createKibanaRequest();
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(
|
|
AuthenticationResult.redirectTo(
|
|
'/mock-server-basepath/security/access_agreement?next=%2Fsome-url',
|
|
{
|
|
user: mockUser,
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
}
|
|
)
|
|
);
|
|
});
|
|
|
|
it('redirects AJAX requests to Access Agreement when needed.', async () => {
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser, {
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
|
|
const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } });
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(
|
|
AuthenticationResult.redirectTo(
|
|
'/mock-server-basepath/security/access_agreement?next=%2Fmock-server-basepath%2Fpath',
|
|
{
|
|
user: mockUser,
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
}
|
|
)
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('with Overwritten Session', () => {
|
|
const mockUser = mockAuthenticatedUser();
|
|
beforeEach(() => {
|
|
mockOptions = getMockOptions({
|
|
providers: {
|
|
basic: { basic1: { order: 0 } },
|
|
saml: { saml1: { order: 1, realm: 'saml1' } },
|
|
},
|
|
});
|
|
mockOptions.session.get.mockResolvedValue(null);
|
|
mockOptions.session.update.mockImplementation(async (request, value) => value);
|
|
mockOptions.session.extend.mockImplementation(async (request, value) => value);
|
|
mockOptions.session.create.mockImplementation(async (request, value) => ({
|
|
...mockSessVal,
|
|
...value,
|
|
}));
|
|
|
|
authenticator = new Authenticator(mockOptions);
|
|
});
|
|
|
|
it('does not redirect to Overwritten Session its own requests', async () => {
|
|
const request = httpServerMock.createKibanaRequest({
|
|
path: '/security/overwritten_session',
|
|
});
|
|
mockOptions.session.get.mockResolvedValue({ ...mockSessVal, username: 'old-username' });
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser, { state: 'some-state' })
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(AuthenticationResult.succeeded(mockUser, { state: 'some-state' }));
|
|
});
|
|
|
|
it('does not redirect to Overwritten Session if username and provider did not change', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser, {
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(
|
|
AuthenticationResult.succeeded(mockUser, {
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
});
|
|
|
|
it('does not redirect to Overwritten Session if session was unauthenticated before login', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue({ ...mockSessVal, username: undefined });
|
|
|
|
const newMockUser = mockAuthenticatedUser({ username: 'new-username' });
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(newMockUser, {
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(
|
|
AuthenticationResult.succeeded(newMockUser, {
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
});
|
|
|
|
it('redirects to Overwritten Session when username changes', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue({ ...mockSessVal, username: 'old-username' });
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser, {
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(
|
|
AuthenticationResult.redirectTo(
|
|
'/mock-server-basepath/security/overwritten_session?next=%2Fmock-server-basepath%2Fpath',
|
|
{
|
|
user: mockUser,
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
}
|
|
)
|
|
);
|
|
});
|
|
|
|
it('redirects to Overwritten Session when provider changes', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue({
|
|
...mockSessVal,
|
|
provider: { type: 'saml', name: 'saml1' },
|
|
});
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser, {
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(
|
|
AuthenticationResult.redirectTo(
|
|
'/mock-server-basepath/security/overwritten_session?next=%2Fmock-server-basepath%2Fpath',
|
|
{
|
|
user: mockUser,
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
}
|
|
)
|
|
);
|
|
});
|
|
|
|
it('redirects to Overwritten Session preserving redirect URL specified in login attempt.', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue({ ...mockSessVal, username: 'old-username' });
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser, {
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, {
|
|
provider: { type: 'basic' },
|
|
value: {},
|
|
redirectURL: '/some-url',
|
|
})
|
|
).resolves.toEqual(
|
|
AuthenticationResult.redirectTo(
|
|
'/mock-server-basepath/security/overwritten_session?next=%2Fsome-url',
|
|
{
|
|
user: mockUser,
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
}
|
|
)
|
|
);
|
|
});
|
|
|
|
it('redirects to Overwritten Session preserving redirect URL specified in the authentication result.', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue({ ...mockSessVal, username: 'old-username' });
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.redirectTo('/some-url', {
|
|
user: mockUser,
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(
|
|
AuthenticationResult.redirectTo(
|
|
'/mock-server-basepath/security/overwritten_session?next=%2Fsome-url',
|
|
{
|
|
user: mockUser,
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
}
|
|
)
|
|
);
|
|
});
|
|
|
|
it('redirects AJAX requests to Overwritten Session when needed.', async () => {
|
|
const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } });
|
|
mockOptions.session.get.mockResolvedValue({ ...mockSessVal, username: 'old-username' });
|
|
|
|
mockBasicAuthenticationProvider.login.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser, {
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
|
|
await expect(
|
|
authenticator.login(request, { provider: { type: 'basic' }, value: {} })
|
|
).resolves.toEqual(
|
|
AuthenticationResult.redirectTo(
|
|
'/mock-server-basepath/security/overwritten_session?next=%2Fmock-server-basepath%2Fpath',
|
|
{
|
|
user: mockUser,
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
}
|
|
)
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('`authenticate` method', () => {
|
|
let authenticator: Authenticator;
|
|
let mockOptions: ReturnType<typeof getMockOptions>;
|
|
let mockSessVal: SessionValue;
|
|
beforeEach(() => {
|
|
mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } });
|
|
mockOptions.session.get.mockResolvedValue(null);
|
|
mockSessVal = sessionMock.createValue({ state: { authorization: 'Basic xxx' } });
|
|
|
|
authenticator = new Authenticator(mockOptions);
|
|
});
|
|
|
|
it('fails if request is not provided.', async () => {
|
|
await expect(authenticator.authenticate(undefined as any)).rejects.toThrowError(
|
|
'Request should be a valid "KibanaRequest" instance, was [undefined].'
|
|
);
|
|
});
|
|
|
|
it('fails if an authentication provider fails.', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
const failureReason = new Error('Not Authorized');
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.failed(failureReason)
|
|
);
|
|
|
|
const authenticationResult = await authenticator.authenticate(request);
|
|
expect(authenticationResult.failed()).toBe(true);
|
|
expect(authenticationResult.error).toBe(failureReason);
|
|
});
|
|
|
|
it('returns user that authentication provider returns.', async () => {
|
|
const request = httpServerMock.createKibanaRequest({
|
|
headers: { authorization: 'Basic ***' },
|
|
});
|
|
|
|
const user = mockAuthenticatedUser();
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } })
|
|
);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.succeeded(user, { authHeaders: { authorization: 'Basic .....' } })
|
|
);
|
|
});
|
|
|
|
it('creates session whenever authentication provider returns state for system API requests', async () => {
|
|
const user = mockAuthenticatedUser();
|
|
const request = httpServerMock.createKibanaRequest({
|
|
headers: { 'kbn-system-request': 'true' },
|
|
});
|
|
const authorization = `Basic ${Buffer.from('foo:bar').toString('base64')}`;
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.succeeded(user, { state: { authorization } })
|
|
);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.succeeded(user, { state: { authorization } })
|
|
);
|
|
|
|
expect(mockOptions.session.create).toHaveBeenCalledTimes(1);
|
|
expect(mockOptions.session.create).toHaveBeenCalledWith(request, {
|
|
username: user.username,
|
|
provider: mockSessVal.provider,
|
|
state: { authorization },
|
|
});
|
|
});
|
|
|
|
it('creates session whenever authentication provider returns state for non-system API requests', async () => {
|
|
const user = mockAuthenticatedUser();
|
|
const request = httpServerMock.createKibanaRequest({
|
|
headers: { 'kbn-system-request': 'false' },
|
|
});
|
|
const authorization = `Basic ${Buffer.from('foo:bar').toString('base64')}`;
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.succeeded(user, { state: { authorization } })
|
|
);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.succeeded(user, { state: { authorization } })
|
|
);
|
|
|
|
expect(mockOptions.session.create).toHaveBeenCalledTimes(1);
|
|
expect(mockOptions.session.create).toHaveBeenCalledWith(request, {
|
|
username: user.username,
|
|
provider: mockSessVal.provider,
|
|
state: { authorization },
|
|
});
|
|
});
|
|
|
|
it('does not extend session for system API calls.', async () => {
|
|
const user = mockAuthenticatedUser();
|
|
const request = httpServerMock.createKibanaRequest({
|
|
headers: { 'kbn-system-request': 'true' },
|
|
});
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.succeeded(user)
|
|
);
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.succeeded(user)
|
|
);
|
|
|
|
expect(mockOptions.session.create).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.update).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.extend).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('extends session for non-system API calls.', async () => {
|
|
const user = mockAuthenticatedUser();
|
|
const request = httpServerMock.createKibanaRequest({
|
|
headers: { 'kbn-system-request': 'false' },
|
|
});
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.succeeded(user)
|
|
);
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.succeeded(user)
|
|
);
|
|
|
|
expect(mockOptions.session.extend).toHaveBeenCalledTimes(1);
|
|
expect(mockOptions.session.extend).toHaveBeenCalledWith(request, mockSessVal);
|
|
expect(mockOptions.session.create).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.update).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not touch session for system API calls if authentication fails with non-401 reason.', async () => {
|
|
const request = httpServerMock.createKibanaRequest({
|
|
headers: { 'kbn-system-request': 'true' },
|
|
});
|
|
|
|
const failureReason = new Error('some error');
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.failed(failureReason)
|
|
);
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.failed(failureReason)
|
|
);
|
|
|
|
expect(mockOptions.session.create).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.update).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.extend).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not touch session for non-system API calls if authentication fails with non-401 reason.', async () => {
|
|
const request = httpServerMock.createKibanaRequest({
|
|
headers: { 'kbn-system-request': 'false' },
|
|
});
|
|
|
|
const failureReason = new Error('some error');
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.failed(failureReason)
|
|
);
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.failed(failureReason)
|
|
);
|
|
|
|
expect(mockOptions.session.create).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.update).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.extend).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('replaces existing session with the one returned by authentication provider for system API requests', async () => {
|
|
const user = mockAuthenticatedUser();
|
|
const newState = { authorization: 'Basic yyy' };
|
|
const request = httpServerMock.createKibanaRequest({
|
|
headers: { 'kbn-system-request': 'true' },
|
|
});
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.succeeded(user, { state: newState })
|
|
);
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.succeeded(user, { state: newState })
|
|
);
|
|
|
|
expect(mockOptions.session.update).toHaveBeenCalledTimes(1);
|
|
expect(mockOptions.session.update).toHaveBeenCalledWith(request, {
|
|
...mockSessVal,
|
|
state: newState,
|
|
});
|
|
expect(mockOptions.session.create).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.extend).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('replaces existing session with the one returned by authentication provider for non-system API requests', async () => {
|
|
const user = mockAuthenticatedUser();
|
|
const newState = { authorization: 'Basic yyy' };
|
|
const request = httpServerMock.createKibanaRequest({
|
|
headers: { 'kbn-system-request': 'false' },
|
|
});
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.succeeded(user, { state: newState })
|
|
);
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.succeeded(user, { state: newState })
|
|
);
|
|
|
|
expect(mockOptions.session.update).toHaveBeenCalledTimes(1);
|
|
expect(mockOptions.session.update).toHaveBeenCalledWith(request, {
|
|
...mockSessVal,
|
|
state: newState,
|
|
});
|
|
expect(mockOptions.session.create).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.extend).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('clears session if provider failed to authenticate system API request with 401 with active session.', async () => {
|
|
const request = httpServerMock.createKibanaRequest({
|
|
headers: { 'kbn-system-request': 'true' },
|
|
});
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.failed(Boom.unauthorized())
|
|
);
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.failed(Boom.unauthorized())
|
|
);
|
|
|
|
expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1);
|
|
expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' });
|
|
expect(mockOptions.session.create).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.update).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.extend).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('clears session if provider failed to authenticate non-system API request with 401 with active session.', async () => {
|
|
const request = httpServerMock.createKibanaRequest({
|
|
headers: { 'kbn-system-request': 'false' },
|
|
});
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.failed(Boom.unauthorized())
|
|
);
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.failed(Boom.unauthorized())
|
|
);
|
|
|
|
expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1);
|
|
expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' });
|
|
expect(mockOptions.session.create).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.update).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.extend).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('clears session if provider requested it via setting state to `null`.', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.redirectTo('some-url', { state: null })
|
|
);
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.redirectTo('some-url', { state: null })
|
|
);
|
|
|
|
expect(mockOptions.session.invalidate).toHaveBeenCalledTimes(1);
|
|
expect(mockOptions.session.invalidate).toHaveBeenCalledWith(request, { match: 'current' });
|
|
expect(mockOptions.session.create).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.update).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.extend).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not clear session if provider can not handle system API request authentication with active session.', async () => {
|
|
const request = httpServerMock.createKibanaRequest({
|
|
headers: { 'kbn-system-request': 'true' },
|
|
});
|
|
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.notHandled()
|
|
);
|
|
|
|
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.create).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.update).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.extend).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not clear session if provider can not handle non-system API request authentication with active session.', async () => {
|
|
const request = httpServerMock.createKibanaRequest({
|
|
headers: { 'kbn-system-request': 'false' },
|
|
});
|
|
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.notHandled()
|
|
);
|
|
|
|
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.create).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.update).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.extend).not.toHaveBeenCalled();
|
|
});
|
|
|
|
describe('with Login Selector', () => {
|
|
beforeEach(() => {
|
|
mockOptions = getMockOptions({
|
|
selector: { enabled: true },
|
|
providers: { basic: { basic1: { order: 0 } } },
|
|
});
|
|
|
|
authenticator = new Authenticator(mockOptions);
|
|
});
|
|
|
|
it('does not redirect to Login Selector if there is an active session', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.notHandled()
|
|
);
|
|
expect(mockBasicAuthenticationProvider.authenticate).toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not redirect AJAX requests to Login Selector', async () => {
|
|
const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } });
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.notHandled()
|
|
);
|
|
expect(mockBasicAuthenticationProvider.authenticate).toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not redirect to Login Selector if request has `Authorization` header', async () => {
|
|
const request = httpServerMock.createKibanaRequest({
|
|
headers: { authorization: 'Basic ***' },
|
|
});
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.notHandled()
|
|
);
|
|
expect(mockBasicAuthenticationProvider.authenticate).toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not redirect to Login Selector if it is not enabled', async () => {
|
|
mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } });
|
|
authenticator = new Authenticator(mockOptions);
|
|
|
|
const request = httpServerMock.createKibanaRequest();
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.notHandled()
|
|
);
|
|
expect(mockBasicAuthenticationProvider.authenticate).toHaveBeenCalled();
|
|
});
|
|
|
|
it('redirects to the Login Selector when needed.', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.redirectTo(
|
|
'/mock-server-basepath/login?next=%2Fmock-server-basepath%2Fpath'
|
|
)
|
|
);
|
|
|
|
// Unauthenticated session should be treated as non-existent one.
|
|
mockOptions.session.get.mockResolvedValue({ ...mockSessVal, username: undefined });
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.redirectTo(
|
|
'/mock-server-basepath/login?next=%2Fmock-server-basepath%2Fpath'
|
|
)
|
|
);
|
|
expect(mockBasicAuthenticationProvider.authenticate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('redirects to the Login Selector with auth provider hint when needed.', async () => {
|
|
const request = httpServerMock.createKibanaRequest({
|
|
query: { [AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER]: 'custom1' },
|
|
});
|
|
|
|
// Includes hint if there is no active session.
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.redirectTo(
|
|
'/mock-server-basepath/login?next=%2Fmock-server-basepath%2Fpath%3Fauth_provider_hint%3Dcustom1&auth_provider_hint=custom1'
|
|
)
|
|
);
|
|
|
|
// Includes hint if session is unauthenticated.
|
|
mockOptions.session.get.mockResolvedValue({ ...mockSessVal, username: undefined });
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.redirectTo(
|
|
'/mock-server-basepath/login?next=%2Fmock-server-basepath%2Fpath%3Fauth_provider_hint%3Dcustom1&auth_provider_hint=custom1'
|
|
)
|
|
);
|
|
|
|
expect(mockBasicAuthenticationProvider.authenticate).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('with Access Agreement', () => {
|
|
const mockUser = mockAuthenticatedUser();
|
|
beforeEach(() => {
|
|
mockOptions = getMockOptions({
|
|
providers: {
|
|
basic: { basic1: { order: 0, accessAgreement: { message: 'some notice' } } },
|
|
},
|
|
});
|
|
mockOptions.license.getFeatures.mockReturnValue({
|
|
allowAccessAgreement: true,
|
|
} as SecurityLicenseFeatures);
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser)
|
|
);
|
|
|
|
authenticator = new Authenticator(mockOptions);
|
|
});
|
|
|
|
it('does not redirect to Access Agreement if there is no active session', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue(null);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.succeeded(mockUser)
|
|
);
|
|
});
|
|
|
|
it('does not redirect AJAX requests to Access Agreement', async () => {
|
|
const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } });
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.succeeded(mockUser)
|
|
);
|
|
});
|
|
|
|
it('does not redirect to Access Agreement if request cannot be handled', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.notHandled()
|
|
);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.notHandled()
|
|
);
|
|
});
|
|
|
|
it('does not redirect to Access Agreement if authentication fails', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
const failureReason = new Error('something went wrong');
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.failed(failureReason)
|
|
);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.failed(failureReason)
|
|
);
|
|
});
|
|
|
|
it('does not redirect to Access Agreement if redirect is required to complete authentication', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.redirectTo('/some-url')
|
|
);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.redirectTo('/some-url')
|
|
);
|
|
});
|
|
|
|
it('does not redirect to Access Agreement if user has already acknowledged it', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue({
|
|
...mockSessVal,
|
|
accessAgreementAcknowledged: true,
|
|
});
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.succeeded(mockUser)
|
|
);
|
|
});
|
|
|
|
it('does not redirect to Access Agreement its own requests', async () => {
|
|
const request = httpServerMock.createKibanaRequest({ path: '/security/access_agreement' });
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.succeeded(mockUser)
|
|
);
|
|
});
|
|
|
|
it('does not redirect to Access Agreement if it is not configured', async () => {
|
|
mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } });
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
authenticator = new Authenticator(mockOptions);
|
|
|
|
const request = httpServerMock.createKibanaRequest();
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.succeeded(mockUser)
|
|
);
|
|
});
|
|
|
|
it('does not redirect to Access Agreement if license doesnt allow it.', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
mockOptions.license.getFeatures.mockReturnValue({
|
|
allowAccessAgreement: false,
|
|
} as SecurityLicenseFeatures);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.succeeded(mockUser)
|
|
);
|
|
});
|
|
|
|
it('redirects to Access Agreement when needed.', async () => {
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
mockOptions.session.extend.mockResolvedValue(mockSessVal);
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser, {
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
|
|
const request = httpServerMock.createKibanaRequest();
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.redirectTo(
|
|
'/mock-server-basepath/security/access_agreement?next=%2Fmock-server-basepath%2Fpath',
|
|
{ user: mockUser, authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' } }
|
|
)
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('with Overwritten Session', () => {
|
|
const mockUser = mockAuthenticatedUser();
|
|
beforeEach(() => {
|
|
mockOptions = getMockOptions({
|
|
providers: {
|
|
basic: { basic1: { order: 0 } },
|
|
saml: { saml1: { order: 1, realm: 'saml1' } },
|
|
},
|
|
});
|
|
mockOptions.session.update.mockImplementation(async (request, value) => value);
|
|
mockOptions.session.extend.mockImplementation(async (request, value) => value);
|
|
mockOptions.session.create.mockImplementation(async (request, value) => ({
|
|
...mockSessVal,
|
|
...value,
|
|
}));
|
|
|
|
authenticator = new Authenticator(mockOptions);
|
|
});
|
|
|
|
it('does not redirect to Overwritten Session its own requests', async () => {
|
|
const request = httpServerMock.createKibanaRequest({
|
|
path: '/security/overwritten_session',
|
|
});
|
|
mockOptions.session.get.mockResolvedValue({ ...mockSessVal, username: 'old-username' });
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser)
|
|
);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.succeeded(mockUser)
|
|
);
|
|
});
|
|
|
|
it('does not redirect AJAX requests to Overwritten Session', async () => {
|
|
const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } });
|
|
mockOptions.session.get.mockResolvedValue({ ...mockSessVal, username: 'old-username' });
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser, {
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.succeeded(mockUser, {
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
});
|
|
|
|
it('does not redirect to Overwritten Session if username and provider did not change', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser, {
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.succeeded(mockUser, {
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
});
|
|
|
|
it('redirects to Overwritten Session when username changes', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue({ ...mockSessVal, username: 'old-username' });
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser, {
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.redirectTo(
|
|
'/mock-server-basepath/security/overwritten_session?next=%2Fmock-server-basepath%2Fpath',
|
|
{
|
|
user: mockUser,
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
}
|
|
)
|
|
);
|
|
});
|
|
|
|
it('redirects to Overwritten Session when provider changes', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue({
|
|
...mockSessVal,
|
|
provider: { type: 'saml', name: 'saml1' },
|
|
});
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.succeeded(mockUser, {
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.redirectTo(
|
|
'/mock-server-basepath/security/overwritten_session?next=%2Fmock-server-basepath%2Fpath',
|
|
{
|
|
user: mockUser,
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
}
|
|
)
|
|
);
|
|
});
|
|
|
|
it('redirects to Overwritten Session preserving redirect URL specified in the authentication result.', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue({ ...mockSessVal, username: 'old-username' });
|
|
|
|
mockBasicAuthenticationProvider.authenticate.mockResolvedValue(
|
|
AuthenticationResult.redirectTo('/some-url', {
|
|
user: mockUser,
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
})
|
|
);
|
|
|
|
await expect(authenticator.authenticate(request)).resolves.toEqual(
|
|
AuthenticationResult.redirectTo(
|
|
'/mock-server-basepath/security/overwritten_session?next=%2Fsome-url',
|
|
{
|
|
user: mockUser,
|
|
state: 'some-state',
|
|
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
|
|
}
|
|
)
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('`logout` method', () => {
|
|
let authenticator: Authenticator;
|
|
let mockOptions: ReturnType<typeof getMockOptions>;
|
|
let mockSessVal: SessionValue;
|
|
beforeEach(() => {
|
|
mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } });
|
|
mockSessVal = sessionMock.createValue({ state: { authorization: 'Basic xxx' } });
|
|
|
|
authenticator = new Authenticator(mockOptions);
|
|
});
|
|
|
|
it('fails if request is not provided.', async () => {
|
|
await expect(authenticator.logout(undefined as any)).rejects.toThrowError(
|
|
'Request should be a valid "KibanaRequest" instance, was [undefined].'
|
|
);
|
|
});
|
|
|
|
it('redirects to login form if session does not exist.', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue(null);
|
|
mockBasicAuthenticationProvider.logout.mockResolvedValue(DeauthenticationResult.notHandled());
|
|
|
|
await expect(authenticator.logout(request)).resolves.toEqual(
|
|
DeauthenticationResult.redirectTo('/mock-server-basepath/login?msg=LOGGED_OUT')
|
|
);
|
|
|
|
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('clears session and returns whatever authentication provider returns.', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockBasicAuthenticationProvider.logout.mockResolvedValue(
|
|
DeauthenticationResult.redirectTo('some-url')
|
|
);
|
|
mockOptions.session.get.mockResolvedValue(mockSessVal);
|
|
|
|
await expect(authenticator.logout(request)).resolves.toEqual(
|
|
DeauthenticationResult.redirectTo('some-url')
|
|
);
|
|
|
|
expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1);
|
|
expect(mockOptions.session.invalidate).toHaveBeenCalled();
|
|
});
|
|
|
|
it('if session does not exist but provider name is valid, returns whatever authentication provider returns.', async () => {
|
|
const request = httpServerMock.createKibanaRequest({
|
|
query: { provider: 'basic1' },
|
|
});
|
|
mockOptions.session.get.mockResolvedValue(null);
|
|
|
|
mockBasicAuthenticationProvider.logout.mockResolvedValue(
|
|
DeauthenticationResult.redirectTo('some-url')
|
|
);
|
|
|
|
await expect(authenticator.logout(request)).resolves.toEqual(
|
|
DeauthenticationResult.redirectTo('some-url')
|
|
);
|
|
|
|
expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1);
|
|
expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledWith(request, null);
|
|
expect(mockOptions.session.invalidate).toHaveBeenCalled();
|
|
});
|
|
|
|
it('if session does not exist and provider name is not available, returns whatever authentication provider returns.', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
mockOptions.session.get.mockResolvedValue(null);
|
|
|
|
mockBasicAuthenticationProvider.logout.mockResolvedValue(
|
|
DeauthenticationResult.redirectTo('some-url')
|
|
);
|
|
|
|
await expect(authenticator.logout(request)).resolves.toEqual(
|
|
DeauthenticationResult.redirectTo('some-url')
|
|
);
|
|
|
|
expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledTimes(1);
|
|
expect(mockBasicAuthenticationProvider.logout).toHaveBeenCalledWith(request);
|
|
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('if session does not exist and providers is empty, redirects to default logout path.', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
|
|
mockOptions = getMockOptions({
|
|
providers: { basic: { basic1: { order: 0, enabled: false } } },
|
|
});
|
|
authenticator = new Authenticator(mockOptions);
|
|
|
|
await expect(authenticator.logout(request)).resolves.toEqual(
|
|
DeauthenticationResult.redirectTo(
|
|
'/mock-server-basepath/security/logged_out?msg=LOGGED_OUT'
|
|
)
|
|
);
|
|
|
|
expect(mockBasicAuthenticationProvider.logout).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.invalidate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('redirects to login form if session does not exist and provider name is invalid', async () => {
|
|
const request = httpServerMock.createKibanaRequest({ query: { provider: 'foo' } });
|
|
mockOptions.session.get.mockResolvedValue(null);
|
|
|
|
await expect(authenticator.logout(request)).resolves.toEqual(
|
|
DeauthenticationResult.redirectTo('/mock-server-basepath/login?msg=LOGGED_OUT')
|
|
);
|
|
|
|
expect(mockBasicAuthenticationProvider.logout).not.toHaveBeenCalled();
|
|
expect(mockOptions.session.invalidate).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('`acknowledgeAccessAgreement` method', () => {
|
|
let authenticator: Authenticator;
|
|
let mockOptions: ReturnType<typeof getMockOptions>;
|
|
let mockSessionValue: SessionValue;
|
|
const auditLogger = {
|
|
log: jest.fn(),
|
|
};
|
|
|
|
beforeEach(() => {
|
|
mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } });
|
|
mockSessionValue = sessionMock.createValue({ state: { authorization: 'Basic xxx' } });
|
|
mockOptions.session.get.mockResolvedValue(mockSessionValue);
|
|
mockOptions.audit.asScoped.mockReturnValue(auditLogger);
|
|
mockOptions.getCurrentUser.mockReturnValue(mockAuthenticatedUser());
|
|
mockOptions.license.getFeatures.mockReturnValue({
|
|
allowAccessAgreement: true,
|
|
} as SecurityLicenseFeatures);
|
|
|
|
authenticator = new Authenticator(mockOptions);
|
|
});
|
|
|
|
it('fails if user is not authenticated', async () => {
|
|
mockOptions.getCurrentUser.mockReturnValue(null);
|
|
|
|
await expect(
|
|
authenticator.acknowledgeAccessAgreement(httpServerMock.createKibanaRequest())
|
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
`"Cannot acknowledge access agreement for unauthenticated user."`
|
|
);
|
|
|
|
expect(mockOptions.session.update).not.toHaveBeenCalled();
|
|
expect(mockOptions.featureUsageService.recordPreAccessAgreementUsage).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('fails if cannot retrieve user session', async () => {
|
|
mockOptions.session.get.mockResolvedValue(null);
|
|
|
|
await expect(
|
|
authenticator.acknowledgeAccessAgreement(httpServerMock.createKibanaRequest())
|
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
`"Cannot acknowledge access agreement for unauthenticated user."`
|
|
);
|
|
|
|
expect(mockOptions.session.update).not.toHaveBeenCalled();
|
|
expect(mockOptions.featureUsageService.recordPreAccessAgreementUsage).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('fails if license does not allow access agreement acknowledgement', async () => {
|
|
mockOptions.license.getFeatures.mockReturnValue({
|
|
allowAccessAgreement: false,
|
|
} as SecurityLicenseFeatures);
|
|
|
|
await expect(
|
|
authenticator.acknowledgeAccessAgreement(httpServerMock.createKibanaRequest())
|
|
).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
`"Current license does not allow access agreement acknowledgement."`
|
|
);
|
|
|
|
expect(mockOptions.session.update).not.toHaveBeenCalled();
|
|
expect(mockOptions.featureUsageService.recordPreAccessAgreementUsage).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('properly acknowledges access agreement for the authenticated user', async () => {
|
|
const request = httpServerMock.createKibanaRequest();
|
|
await authenticator.acknowledgeAccessAgreement(request);
|
|
|
|
expect(mockOptions.session.update).toHaveBeenCalledTimes(1);
|
|
expect(mockOptions.session.update).toHaveBeenCalledWith(request, {
|
|
...mockSessionValue,
|
|
accessAgreementAcknowledged: true,
|
|
});
|
|
|
|
expect(auditLogger.log).toHaveBeenCalledTimes(1);
|
|
expect(auditLogger.log).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
event: { action: 'access_agreement_acknowledged', category: ['authentication'] },
|
|
})
|
|
);
|
|
|
|
expect(mockOptions.featureUsageService.recordPreAccessAgreementUsage).toHaveBeenCalledTimes(
|
|
1
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('`getRequestOriginalURL` method', () => {
|
|
let authenticator: Authenticator;
|
|
let mockOptions: ReturnType<typeof getMockOptions>;
|
|
beforeEach(() => {
|
|
mockOptions = getMockOptions({ providers: { basic: { basic1: { order: 0 } } } });
|
|
authenticator = new Authenticator(mockOptions);
|
|
});
|
|
|
|
it('filters out auth specific query parameters', () => {
|
|
expect(authenticator.getRequestOriginalURL(httpServerMock.createKibanaRequest())).toBe(
|
|
'/mock-server-basepath/path'
|
|
);
|
|
|
|
expect(
|
|
authenticator.getRequestOriginalURL(
|
|
httpServerMock.createKibanaRequest({
|
|
query: {
|
|
[AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER]: 'saml1',
|
|
},
|
|
})
|
|
)
|
|
).toBe('/mock-server-basepath/path');
|
|
|
|
expect(
|
|
authenticator.getRequestOriginalURL(
|
|
httpServerMock.createKibanaRequest({
|
|
query: {
|
|
[AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER]: 'saml1',
|
|
[AUTH_URL_HASH_QUERY_STRING_PARAMETER]: '#some-hash',
|
|
},
|
|
})
|
|
)
|
|
).toBe('/mock-server-basepath/path');
|
|
});
|
|
|
|
it('allows to include additional query parameters', () => {
|
|
expect(
|
|
authenticator.getRequestOriginalURL(httpServerMock.createKibanaRequest(), [
|
|
['some-param', 'some-value'],
|
|
['some-param2', 'some-value2'],
|
|
])
|
|
).toBe('/mock-server-basepath/path?some-param=some-value&some-param2=some-value2');
|
|
|
|
expect(
|
|
authenticator.getRequestOriginalURL(
|
|
httpServerMock.createKibanaRequest({
|
|
query: {
|
|
[AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER]: 'saml1',
|
|
[AUTH_URL_HASH_QUERY_STRING_PARAMETER]: '#some-hash',
|
|
},
|
|
}),
|
|
[
|
|
['some-param', 'some-value'],
|
|
[AUTH_PROVIDER_HINT_QUERY_STRING_PARAMETER, 'oidc1'],
|
|
]
|
|
)
|
|
).toBe('/mock-server-basepath/path?some-param=some-value&auth_provider_hint=oidc1');
|
|
});
|
|
});
|
|
});
|