Introduce PKI authentication provider. (#42606)

This commit is contained in:
Aleh Zasypkin 2019-08-27 18:28:54 +02:00 committed by GitHub
parent 3cc4653c96
commit 17106e8a78
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1482 additions and 2 deletions

View file

@ -18,6 +18,7 @@
*/
import { Request } from 'hapi';
import { merge } from 'lodash';
import { Socket } from 'net';
import querystring from 'querystring';
@ -37,6 +38,7 @@ interface RequestFixtureOptions {
query?: Record<string, any>;
path?: string;
method?: RouteMethod;
socket?: Socket;
}
function createKibanaRequestMock({
@ -46,6 +48,7 @@ function createKibanaRequestMock({
body = {},
query = {},
method = 'get',
socket = new Socket(),
}: RequestFixtureOptions = {}) {
const queryString = querystring.stringify(query);
return KibanaRequest.from(
@ -63,7 +66,7 @@ function createKibanaRequestMock({
},
route: { settings: {} },
raw: {
req: {},
req: { socket },
},
} as any,
{

View file

@ -536,5 +536,21 @@
fmt: '/_security/api_key',
},
});
/**
* Gets an access token in exchange to the certificate chain for the target subject distinguished name.
*
* @param {string[]} x509_certificate_chain An ordered array of base64-encoded (Section 4 of RFC4648 - not
* base64url-encoded) DER PKIX certificate values.
*
* @returns {{access_token: string, type: string, expires_in: number}}
*/
shield.delegatePKI = ca({
method: 'POST',
needBody: true,
url: {
fmt: '/_security/delegate_pki',
},
});
};
}));

View file

@ -25,6 +25,7 @@ import {
SAMLAuthenticationProvider,
TokenAuthenticationProvider,
OIDCAuthenticationProvider,
PKIAuthenticationProvider,
isSAMLRequestQuery,
} from './providers';
import { AuthenticationResult } from './authentication_result';
@ -98,6 +99,7 @@ const providerMap = new Map<
['saml', SAMLAuthenticationProvider],
['token', TokenAuthenticationProvider],
['oidc', OIDCAuthenticationProvider],
['pki', PKIAuthenticationProvider],
]);
function assertRequest(request: KibanaRequest) {

View file

@ -7,12 +7,20 @@
import sinon from 'sinon';
import { ScopedClusterClient } from '../../../../../../src/core/server';
import { Tokens } from '../tokens';
import { loggingServiceMock, httpServiceMock } from '../../../../../../src/core/server/mocks';
import {
loggingServiceMock,
httpServiceMock,
elasticsearchServiceMock,
} from '../../../../../../src/core/server/mocks';
export type MockAuthenticationProviderOptions = ReturnType<
typeof mockAuthenticationProviderOptions
>;
export type MockAuthenticationProviderOptionsWithJest = ReturnType<
typeof mockAuthenticationProviderOptionsWithJest
>;
export function mockScopedClusterClient(
client: MockAuthenticationProviderOptions['client'],
requestMatcher: sinon.SinonMatcher = sinon.match.any
@ -34,3 +42,16 @@ export function mockAuthenticationProviderOptions() {
tokens: sinon.createStubInstance(Tokens),
};
}
// Will be renamed to mockAuthenticationProviderOptions as soon as we migrate all providers tests to Jest.
export function mockAuthenticationProviderOptionsWithJest() {
const basePath = httpServiceMock.createSetupContract().basePath;
basePath.get.mockReturnValue('/base-path');
return {
client: elasticsearchServiceMock.createClusterClient(),
logger: loggingServiceMock.create().get(),
basePath,
tokens: { refresh: jest.fn(), invalidate: jest.fn() },
};
}

View file

@ -14,3 +14,4 @@ export { KerberosAuthenticationProvider } from './kerberos';
export { SAMLAuthenticationProvider, isSAMLRequestQuery } from './saml';
export { TokenAuthenticationProvider } from './token';
export { OIDCAuthenticationProvider, OIDCAuthenticationFlow } from './oidc';
export { PKIAuthenticationProvider } from './pki';

View file

@ -0,0 +1,589 @@
/*
* 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.
*/
jest.mock('net');
jest.mock('tls');
import { PeerCertificate, TLSSocket } from 'tls';
import { errors } from 'elasticsearch';
import { elasticsearchServiceMock, httpServerMock } from '../../../../../../src/core/server/mocks';
import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock';
import {
MockAuthenticationProviderOptionsWithJest,
mockAuthenticationProviderOptionsWithJest,
} from './base.mock';
import { PKIAuthenticationProvider } from './pki';
import {
ElasticsearchErrorHelpers,
ScopedClusterClient,
} from '../../../../../../src/core/server/elasticsearch';
import { Socket } from 'net';
import { getErrorStatusCode } from '../../errors';
interface MockPeerCertificate extends Partial<PeerCertificate> {
issuerCertificate: MockPeerCertificate;
fingerprint256: string;
}
function getMockPeerCertificate(chain: string[] | string) {
const mockPeerCertificate = {} as MockPeerCertificate;
(Array.isArray(chain) ? chain : [chain]).reduce(
(certificate, fingerprint, index, fingerprintChain) => {
certificate.fingerprint256 = fingerprint;
certificate.raw = { toString: (enc: string) => `fingerprint:${fingerprint}:${enc}` };
// Imitate self-signed certificate that is issuer for itself.
certificate.issuerCertificate = index === fingerprintChain.length - 1 ? certificate : {};
return certificate.issuerCertificate;
},
mockPeerCertificate as Record<string, any>
);
return mockPeerCertificate;
}
function getMockSocket({
authorized = false,
peerCertificate = null,
}: {
authorized?: boolean;
peerCertificate?: MockPeerCertificate | null;
} = {}) {
const socket = new TLSSocket(new Socket());
socket.authorized = authorized;
socket.getPeerCertificate = jest.fn().mockReturnValue(peerCertificate);
return socket;
}
describe('PKIAuthenticationProvider', () => {
let provider: PKIAuthenticationProvider;
let mockOptions: MockAuthenticationProviderOptionsWithJest;
beforeEach(() => {
mockOptions = mockAuthenticationProviderOptionsWithJest();
provider = new PKIAuthenticationProvider(mockOptions);
});
afterEach(() => jest.clearAllMocks());
describe('`authenticate` method', () => {
it('does not handle `authorization` header with unsupported schema even if state contains a valid token.', async () => {
const request = httpServerMock.createKibanaRequest({
headers: { authorization: 'Basic some:credentials' },
});
const state = {
accessToken: 'some-valid-token',
peerCertificateFingerprint256: '2A:7A:C2:DD',
};
const authenticationResult = await provider.authenticate(request, state);
expect(mockOptions.client.asScoped).not.toHaveBeenCalled();
expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled();
expect(request.headers.authorization).toBe('Basic some:credentials');
expect(authenticationResult.notHandled()).toBe(true);
});
it('does not handle requests without certificate.', async () => {
const request = httpServerMock.createKibanaRequest({
socket: getMockSocket({ authorized: true }),
});
const authenticationResult = await provider.authenticate(request, null);
expect(authenticationResult.notHandled()).toBe(true);
expect(mockOptions.client.asScoped).not.toHaveBeenCalled();
expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled();
});
it('does not handle unauthorized requests.', async () => {
const request = httpServerMock.createKibanaRequest({
socket: getMockSocket({ peerCertificate: getMockPeerCertificate('2A:7A:C2:DD') }),
});
const authenticationResult = await provider.authenticate(request, null);
expect(authenticationResult.notHandled()).toBe(true);
expect(mockOptions.client.asScoped).not.toHaveBeenCalled();
expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled();
});
it('fails with non-401 error if state is available, peer is authorized, but certificate is not available.', async () => {
const request = httpServerMock.createKibanaRequest({
socket: getMockSocket({ authorized: true }),
});
const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' };
const authenticationResult = await provider.authenticate(request, state);
expect(authenticationResult.failed()).toBe(true);
expect(authenticationResult.error).toMatchInlineSnapshot(
`[Error: Peer certificate is not available]`
);
expect(authenticationResult.authResponseHeaders).toBeUndefined();
expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled();
});
it('invalidates token and fails with 401 if state is present, but peer certificate is not.', async () => {
const request = httpServerMock.createKibanaRequest({ socket: getMockSocket() });
const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' };
const authenticationResult = await provider.authenticate(request, state);
expect(authenticationResult.failed()).toBe(true);
expect(getErrorStatusCode(authenticationResult.error)).toBe(401);
expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({
accessToken: state.accessToken,
});
});
it('invalidates token and fails with 401 if new certificate is present, but not authorized.', async () => {
const request = httpServerMock.createKibanaRequest({
socket: getMockSocket({ peerCertificate: getMockPeerCertificate('2A:7A:C2:DD') }),
});
const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' };
const authenticationResult = await provider.authenticate(request, state);
expect(authenticationResult.failed()).toBe(true);
expect(getErrorStatusCode(authenticationResult.error)).toBe(401);
expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({
accessToken: state.accessToken,
});
});
it('gets an access token in exchange to peer certificate chain and stores it in the state.', async () => {
const user = mockAuthenticatedUser();
const request = httpServerMock.createKibanaRequest({
headers: {},
socket: getMockSocket({
authorized: true,
peerCertificate: getMockPeerCertificate(['2A:7A:C2:DD', '3B:8B:D3:EE']),
}),
});
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user);
mockOptions.client.asScoped.mockReturnValue(
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
);
mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' });
const authenticationResult = await provider.authenticate(request);
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', {
body: {
x509_certificate_chain: [
'fingerprint:2A:7A:C2:DD:base64',
'fingerprint:3B:8B:D3:EE:base64',
],
},
});
expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1);
expect(mockOptions.client.asScoped).toHaveBeenCalledWith({
headers: { authorization: `Bearer access-token` },
});
expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate');
expect(request.headers).not.toHaveProperty('authorization');
expect(authenticationResult.succeeded()).toBe(true);
expect(authenticationResult.user).toBe(user);
expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' });
expect(authenticationResult.authResponseHeaders).toBeUndefined();
expect(authenticationResult.state).toEqual({
accessToken: 'access-token',
peerCertificateFingerprint256: '2A:7A:C2:DD',
});
});
it('gets an access token in exchange to a self-signed certificate and stores it in the state.', async () => {
const user = mockAuthenticatedUser();
const request = httpServerMock.createKibanaRequest({
headers: {},
socket: getMockSocket({
authorized: true,
peerCertificate: getMockPeerCertificate('2A:7A:C2:DD'),
}),
});
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user);
mockOptions.client.asScoped.mockReturnValue(
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
);
mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' });
const authenticationResult = await provider.authenticate(request);
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', {
body: { x509_certificate_chain: ['fingerprint:2A:7A:C2:DD:base64'] },
});
expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1);
expect(mockOptions.client.asScoped).toHaveBeenCalledWith({
headers: { authorization: `Bearer access-token` },
});
expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate');
expect(request.headers).not.toHaveProperty('authorization');
expect(authenticationResult.succeeded()).toBe(true);
expect(authenticationResult.user).toBe(user);
expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' });
expect(authenticationResult.authResponseHeaders).toBeUndefined();
expect(authenticationResult.state).toEqual({
accessToken: 'access-token',
peerCertificateFingerprint256: '2A:7A:C2:DD',
});
});
it('invalidates existing token and gets a new one if fingerprints do not match.', async () => {
const user = mockAuthenticatedUser();
const request = httpServerMock.createKibanaRequest({
socket: getMockSocket({
authorized: true,
peerCertificate: getMockPeerCertificate(['2A:7A:C2:DD', '3B:8B:D3:EE']),
}),
});
const state = { accessToken: 'existing-token', peerCertificateFingerprint256: '3A:9A:C5:DD' };
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user);
mockOptions.client.asScoped.mockReturnValue(
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
);
mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' });
const authenticationResult = await provider.authenticate(request, state);
expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({
accessToken: state.accessToken,
});
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', {
body: {
x509_certificate_chain: [
'fingerprint:2A:7A:C2:DD:base64',
'fingerprint:3B:8B:D3:EE:base64',
],
},
});
expect(request.headers).not.toHaveProperty('authorization');
expect(authenticationResult.succeeded()).toBe(true);
expect(authenticationResult.user).toBe(user);
expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' });
expect(authenticationResult.authResponseHeaders).toBeUndefined();
expect(authenticationResult.state).toEqual({
accessToken: 'access-token',
peerCertificateFingerprint256: '2A:7A:C2:DD',
});
});
it('gets a new access token even if existing token is expired.', async () => {
const user = mockAuthenticatedUser();
const request = httpServerMock.createKibanaRequest({
socket: getMockSocket({
authorized: true,
peerCertificate: getMockPeerCertificate(['2A:7A:C2:DD', '3B:8B:D3:EE']),
}),
});
const state = { accessToken: 'existing-token', peerCertificateFingerprint256: '2A:7A:C2:DD' };
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser
// In response to call with an expired token.
.mockRejectedValueOnce(ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error()))
// In response to a call with a new token.
.mockResolvedValueOnce(user);
mockOptions.client.asScoped.mockReturnValue(
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
);
mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' });
const authenticationResult = await provider.authenticate(request, state);
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', {
body: {
x509_certificate_chain: [
'fingerprint:2A:7A:C2:DD:base64',
'fingerprint:3B:8B:D3:EE:base64',
],
},
});
expect(request.headers).not.toHaveProperty('authorization');
expect(authenticationResult.succeeded()).toBe(true);
expect(authenticationResult.user).toBe(user);
expect(authenticationResult.authHeaders).toEqual({ authorization: 'Bearer access-token' });
expect(authenticationResult.authResponseHeaders).toBeUndefined();
expect(authenticationResult.state).toEqual({
accessToken: 'access-token',
peerCertificateFingerprint256: '2A:7A:C2:DD',
});
});
it('fails with 401 if existing token is expired, but certificate is not present.', async () => {
const request = httpServerMock.createKibanaRequest({ socket: getMockSocket() });
const state = { accessToken: 'existing-token', peerCertificateFingerprint256: '2A:7A:C2:DD' };
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(
ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())
);
mockOptions.client.asScoped.mockReturnValue(
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
);
const authenticationResult = await provider.authenticate(request, state);
expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled();
expect(request.headers).not.toHaveProperty('authorization');
expect(authenticationResult.failed()).toBe(true);
expect(getErrorStatusCode(authenticationResult.error)).toBe(401);
expect(authenticationResult.authResponseHeaders).toBeUndefined();
});
it('fails if could not retrieve an access token in exchange to peer certificate chain.', async () => {
const request = httpServerMock.createKibanaRequest({
socket: getMockSocket({
authorized: true,
peerCertificate: getMockPeerCertificate('2A:7A:C2:DD'),
}),
});
const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error());
mockOptions.client.callAsInternalUser.mockRejectedValue(failureReason);
const authenticationResult = await provider.authenticate(request);
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', {
body: { x509_certificate_chain: ['fingerprint:2A:7A:C2:DD:base64'] },
});
expect(request.headers).not.toHaveProperty('authorization');
expect(authenticationResult.failed()).toBe(true);
expect(authenticationResult.error).toBe(failureReason);
expect(authenticationResult.authResponseHeaders).toBeUndefined();
});
it('fails if could not retrieve user using the new access token.', async () => {
const request = httpServerMock.createKibanaRequest({
headers: {},
socket: getMockSocket({
authorized: true,
peerCertificate: getMockPeerCertificate('2A:7A:C2:DD'),
}),
});
const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error());
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason);
mockOptions.client.asScoped.mockReturnValue(
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
);
mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' });
const authenticationResult = await provider.authenticate(request);
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', {
body: { x509_certificate_chain: ['fingerprint:2A:7A:C2:DD:base64'] },
});
expect(mockOptions.client.asScoped).toHaveBeenCalledTimes(1);
expect(mockOptions.client.asScoped).toHaveBeenCalledWith({
headers: { authorization: `Bearer access-token` },
});
expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledTimes(1);
expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.authenticate');
expect(request.headers).not.toHaveProperty('authorization');
expect(authenticationResult.failed()).toBe(true);
expect(authenticationResult.error).toBe(failureReason);
expect(authenticationResult.authResponseHeaders).toBeUndefined();
});
it('succeeds if state contains a valid token.', async () => {
const user = mockAuthenticatedUser();
const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' };
const request = httpServerMock.createKibanaRequest({
socket: getMockSocket({
authorized: true,
peerCertificate: getMockPeerCertificate(state.peerCertificateFingerprint256),
}),
});
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user);
mockOptions.client.asScoped.mockReturnValue(
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
);
const authenticationResult = await provider.authenticate(request, state);
expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled();
expect(request.headers).not.toHaveProperty('authorization');
expect(authenticationResult.succeeded()).toBe(true);
expect(authenticationResult.authHeaders).toEqual({
authorization: `Bearer ${state.accessToken}`,
});
expect(authenticationResult.user).toBe(user);
expect(authenticationResult.state).toBeUndefined();
});
it('fails if token from the state is rejected because of unknown reason.', async () => {
const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' };
const request = httpServerMock.createKibanaRequest({
socket: getMockSocket({
authorized: true,
peerCertificate: getMockPeerCertificate(state.peerCertificateFingerprint256),
}),
});
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(new errors.ServiceUnavailable());
mockOptions.client.asScoped.mockReturnValue(
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
);
const authenticationResult = await provider.authenticate(request, state);
expect(authenticationResult.failed()).toBe(true);
expect(authenticationResult.error).toHaveProperty('status', 503);
expect(authenticationResult.authResponseHeaders).toBeUndefined();
});
it('succeeds if `authorization` contains a valid token.', async () => {
const user = mockAuthenticatedUser();
const request = httpServerMock.createKibanaRequest({
headers: { authorization: 'Bearer some-valid-token' },
});
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockResolvedValue(user);
mockOptions.client.asScoped.mockReturnValue(
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
);
const authenticationResult = await provider.authenticate(request);
expect(request.headers.authorization).toBe('Bearer some-valid-token');
expect(authenticationResult.succeeded()).toBe(true);
expect(authenticationResult.authHeaders).toBeUndefined();
expect(authenticationResult.user).toBe(user);
expect(authenticationResult.state).toBeUndefined();
});
it('fails if token from `authorization` header is rejected.', async () => {
const request = httpServerMock.createKibanaRequest({
headers: { authorization: 'Bearer some-invalid-token' },
});
const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error());
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason);
mockOptions.client.asScoped.mockReturnValue(
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
);
const authenticationResult = await provider.authenticate(request);
expect(authenticationResult.failed()).toBe(true);
expect(authenticationResult.error).toBe(failureReason);
});
it('fails if token from `authorization` header is rejected even if state contains a valid one.', async () => {
const user = mockAuthenticatedUser();
const state = { accessToken: 'token', peerCertificateFingerprint256: '2A:7A:C2:DD' };
const request = httpServerMock.createKibanaRequest({
headers: { authorization: 'Bearer some-invalid-token' },
socket: getMockSocket({
authorized: true,
peerCertificate: getMockPeerCertificate(state.peerCertificateFingerprint256),
}),
});
const failureReason = ElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error());
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser
// In response to call with a token from header.
.mockRejectedValueOnce(failureReason)
// In response to a call with a token from session (not expected to be called).
.mockResolvedValueOnce(user);
mockOptions.client.asScoped.mockReturnValue(
(mockScopedClusterClient as unknown) as jest.Mocked<ScopedClusterClient>
);
const authenticationResult = await provider.authenticate(request, state);
expect(authenticationResult.failed()).toBe(true);
expect(authenticationResult.error).toBe(failureReason);
});
});
describe('`logout` method', () => {
it('returns `notHandled` if state is not presented.', async () => {
const request = httpServerMock.createKibanaRequest();
let deauthenticateResult = await provider.logout(request);
expect(deauthenticateResult.notHandled()).toBe(true);
deauthenticateResult = await provider.logout(request, null);
expect(deauthenticateResult.notHandled()).toBe(true);
expect(mockOptions.tokens.invalidate).not.toHaveBeenCalled();
});
it('fails if `tokens.invalidate` fails', async () => {
const request = httpServerMock.createKibanaRequest();
const state = { accessToken: 'foo', peerCertificateFingerprint256: '2A:7A:C2:DD' };
const failureReason = new Error('failed to delete token');
mockOptions.tokens.invalidate.mockRejectedValue(failureReason);
const authenticationResult = await provider.logout(request, state);
expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ accessToken: 'foo' });
expect(authenticationResult.failed()).toBe(true);
expect(authenticationResult.error).toBe(failureReason);
});
it('redirects to `/logged_out` page if access token is invalidated successfully.', async () => {
const request = httpServerMock.createKibanaRequest();
const state = { accessToken: 'foo', peerCertificateFingerprint256: '2A:7A:C2:DD' };
mockOptions.tokens.invalidate.mockResolvedValue(undefined);
const authenticationResult = await provider.logout(request, state);
expect(mockOptions.tokens.invalidate).toHaveBeenCalledTimes(1);
expect(mockOptions.tokens.invalidate).toHaveBeenCalledWith({ accessToken: 'foo' });
expect(authenticationResult.redirected()).toBe(true);
expect(authenticationResult.redirectURL).toBe('/logged_out');
});
});
});

View file

@ -0,0 +1,277 @@
/*
* 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 Boom from 'boom';
import { DetailedPeerCertificate } from 'tls';
import { KibanaRequest } from '../../../../../../src/core/server';
import { AuthenticationResult } from '../authentication_result';
import { DeauthenticationResult } from '../deauthentication_result';
import { BaseAuthenticationProvider } from './base';
import { Tokens } from '../tokens';
/**
* The state supported by the provider.
*/
interface ProviderState {
/**
* Access token we got in exchange to peer certificate chain.
*/
accessToken: string;
/**
* The SHA-256 digest of the DER encoded peer leaf certificate. It is a `:` separated hexadecimal string.
*/
peerCertificateFingerprint256: string;
}
/**
* Parses request's `Authorization` HTTP header if present and extracts authentication scheme.
* @param request Request instance to extract authentication scheme for.
*/
function getRequestAuthenticationScheme(request: KibanaRequest) {
const authorization = request.headers.authorization;
if (!authorization || typeof authorization !== 'string') {
return '';
}
return authorization.split(/\s+/)[0].toLowerCase();
}
/**
* Provider that supports PKI request authentication.
*/
export class PKIAuthenticationProvider extends BaseAuthenticationProvider {
/**
* Performs PKI request authentication.
* @param request Request instance.
* @param [state] Optional state object associated with the provider.
*/
public async authenticate(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to authenticate user request to ${request.url.path}.`);
const authenticationScheme = getRequestAuthenticationScheme(request);
if (authenticationScheme && authenticationScheme !== 'bearer') {
this.logger.debug(`Unsupported authentication scheme: ${authenticationScheme}`);
return AuthenticationResult.notHandled();
}
let authenticationResult = AuthenticationResult.notHandled();
if (authenticationScheme) {
// We should get rid of `Bearer` scheme support as soon as Reporting doesn't need it anymore.
authenticationResult = await this.authenticateWithBearerScheme(request);
}
if (state && authenticationResult.notHandled()) {
authenticationResult = await this.authenticateViaState(request, state);
// If access token expired or doesn't match to the certificate fingerprint we should try to get
// a new one in exchange to peer certificate chain.
if (
authenticationResult.notHandled() ||
(authenticationResult.failed() &&
Tokens.isAccessTokenExpiredError(authenticationResult.error))
) {
authenticationResult = await this.authenticateViaPeerCertificate(request);
// If we have an active session that we couldn't use to authenticate user and at the same time
// we couldn't use peer's certificate to establish a new one, then we should respond with 401
// and force authenticator to clear the session.
if (authenticationResult.notHandled()) {
return AuthenticationResult.failed(Boom.unauthorized());
}
}
}
// If we couldn't authenticate by means of all methods above, let's try to check if we can authenticate
// request using its peer certificate chain, otherwise just return authentication result we have.
return authenticationResult.notHandled()
? await this.authenticateViaPeerCertificate(request)
: authenticationResult;
}
/**
* Invalidates access token retrieved in exchange for peer certificate chain if it exists.
* @param request Request instance.
* @param state State value previously stored by the provider.
*/
public async logout(request: KibanaRequest, state?: ProviderState | null) {
this.logger.debug(`Trying to log user out via ${request.url.path}.`);
if (!state) {
this.logger.debug('There is no access token to invalidate.');
return DeauthenticationResult.notHandled();
}
try {
await this.options.tokens.invalidate({ accessToken: state.accessToken });
} catch (err) {
this.logger.debug(`Failed invalidating access token: ${err.message}`);
return DeauthenticationResult.failed(err);
}
return DeauthenticationResult.redirectTo('/logged_out');
}
/**
* Tries to authenticate request with `Bearer ***` Authorization header by passing it to the Elasticsearch backend.
* @param request Request instance.
*/
private async authenticateWithBearerScheme(request: KibanaRequest) {
this.logger.debug('Trying to authenticate request using "Bearer" authentication scheme.');
try {
const user = await this.getUser(request);
this.logger.debug('Request has been authenticated using "Bearer" authentication scheme.');
return AuthenticationResult.succeeded(user);
} catch (err) {
this.logger.debug(
`Failed to authenticate request using "Bearer" authentication scheme: ${err.message}`
);
return AuthenticationResult.failed(err);
}
}
/**
* Tries to extract access token from state and adds it to the request before it's
* forwarded to Elasticsearch backend.
* @param request Request instance.
* @param state State value previously stored by the provider.
*/
private async authenticateViaState(
request: KibanaRequest,
{ accessToken, peerCertificateFingerprint256 }: ProviderState
) {
this.logger.debug('Trying to authenticate via state.');
// If peer is authorized, but its certificate isn't available, that likely means the connection
// with the peer is closed already. We shouldn't invalidate peer's access token in this case
// since we cannot guarantee that there is a mismatch in access token and peer certificate.
const peerCertificate = request.socket.getPeerCertificate(true);
if (peerCertificate === null && request.socket.authorized) {
this.logger.debug(
'Cannot validate state access token with the peer certificate since it is not available.'
);
return AuthenticationResult.failed(new Error('Peer certificate is not available'));
}
if (
!request.socket.authorized ||
peerCertificate === null ||
(peerCertificate as any).fingerprint256 !== peerCertificateFingerprint256
) {
this.logger.debug(
'Peer certificate is not present or its fingerprint does not match to the one associated with the access token. Invalidating access token...'
);
try {
await this.options.tokens.invalidate({ accessToken });
} catch (err) {
this.logger.debug(`Failed to invalidate access token: ${err.message}`);
return AuthenticationResult.failed(err);
}
// Return "Not Handled" result to allow provider to try to exchange new peer certificate chain
// to the new access token down the line.
return AuthenticationResult.notHandled();
}
try {
const authHeaders = { authorization: `Bearer ${accessToken}` };
const user = await this.getUser(request, authHeaders);
this.logger.debug('Request has been authenticated via state.');
return AuthenticationResult.succeeded(user, { authHeaders });
} catch (err) {
this.logger.debug(`Failed to authenticate request via state: ${err.message}`);
return AuthenticationResult.failed(err);
}
}
/**
* Tries to exchange peer certificate chain to access/refresh token pair.
* @param request Request instance.
*/
private async authenticateViaPeerCertificate(request: KibanaRequest) {
this.logger.debug('Trying to authenticate request via peer certificate chain.');
if (!request.socket.authorized) {
this.logger.debug(
`Authentication is not possible since peer certificate was not authorized: ${request.socket.authorizationError}.`
);
return AuthenticationResult.notHandled();
}
const peerCertificate = request.socket.getPeerCertificate(true);
if (peerCertificate === null) {
this.logger.debug('Authentication is not possible due to missing peer certificate chain.');
return AuthenticationResult.notHandled();
}
// We should collect entire certificate chain as an ordered array of certificates encoded as base64 strings.
const certificateChain = this.getCertificateChain(peerCertificate);
let accessToken: string;
try {
accessToken = (await this.options.client.callAsInternalUser('shield.delegatePKI', {
body: { x509_certificate_chain: certificateChain },
})).access_token;
} catch (err) {
this.logger.debug(
`Failed to exchange peer certificate chain to an access token: ${err.message}`
);
return AuthenticationResult.failed(err);
}
this.logger.debug('Successfully retrieved access token in exchange to peer certificate chain.');
try {
// Then attempt to query for the user details using the new token
const authHeaders = { authorization: `Bearer ${accessToken}` };
const user = await this.getUser(request, authHeaders);
this.logger.debug('User has been authenticated with new access token');
return AuthenticationResult.succeeded(user, {
authHeaders,
state: {
accessToken,
// NodeJS typings don't include `fingerprint256` yet.
peerCertificateFingerprint256: (peerCertificate as any).fingerprint256,
},
});
} catch (err) {
this.logger.debug(`Failed to authenticate request via access token: ${err.message}`);
return AuthenticationResult.failed(err);
}
}
/**
* Starts from the leaf peer certificate and iterates up to the top-most available certificate
* authority using `issuerCertificate` certificate property. THe iteration is stopped only when
* we detect circular reference (root/self-signed certificate) or when `issuerCertificate` isn't
* available (null or empty object).
* @param peerCertificate Peer leaf certificate instance.
*/
private getCertificateChain(peerCertificate: DetailedPeerCertificate | null) {
const certificateChain = [];
let certificate: DetailedPeerCertificate | null = peerCertificate;
while (certificate !== null && Object.keys(certificate).length > 0) {
certificateChain.push(certificate.raw.toString('base64'));
// For self-signed certificates, `issuerCertificate` may be a circular reference.
if (certificate === certificate.issuerCertificate) {
this.logger.debug('Self-signed certificate is detected in certificate chain');
certificate = null;
} else {
certificate = certificate.issuerCertificate;
}
}
this.logger.debug(
`Peer certificate chain consists of ${certificateChain.length} certificates.`
);
return certificateChain;
}
}

View file

@ -21,6 +21,7 @@ require('@kbn/test').runTestsCli([
require.resolve('../test/token_api_integration/config.js'),
require.resolve('../test/oidc_api_integration/config.ts'),
require.resolve('../test/oidc_api_integration/implicit_flow.config.ts'),
// require.resolve('../test/pki_api_integration/config.ts'),
require.resolve('../test/spaces_api_integration/spaces_only/config'),
require.resolve('../test/spaces_api_integration/security_and_spaces/config_trial'),
require.resolve('../test/spaces_api_integration/security_and_spaces/config_basic'),

View file

@ -0,0 +1,14 @@
/*
* 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 { FtrProviderContext } from '../ftr_provider_context';
export default function({ loadTestFile }: FtrProviderContext) {
describe('apis PKI', function() {
this.tags('ciGroup6');
loadTestFile(require.resolve('./security'));
});
}

View file

@ -0,0 +1,13 @@
/*
* 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 { FtrProviderContext } from '../../ftr_provider_context';
export default function({ loadTestFile }: FtrProviderContext) {
describe('security', () => {
loadTestFile(require.resolve('./pki_auth'));
});
}

View file

@ -0,0 +1,370 @@
/*
* 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 request, { Cookie } from 'request';
import { delay } from 'bluebird';
import { readFileSync } from 'fs';
import { resolve } from 'path';
// @ts-ignore
import { CA_CERT_PATH } from '@kbn/dev-utils';
import { FtrProviderContext } from '../../ftr_provider_context';
const CA_CERT = readFileSync(CA_CERT_PATH);
const FIRST_CLIENT_CERT = readFileSync(resolve(__dirname, '../../fixtures/first_client.p12'));
const SECOND_CLIENT_CERT = readFileSync(resolve(__dirname, '../../fixtures/second_client.p12'));
const UNTRUSTED_CLIENT_CERT = readFileSync(
resolve(__dirname, '../../fixtures/untrusted_client.p12')
);
export default function({ getService }: FtrProviderContext) {
const supertest = getService('supertestWithoutAuth');
const config = getService('config');
function checkCookieIsSet(cookie: Cookie) {
expect(cookie.value).to.not.be.empty();
expect(cookie.key).to.be('sid');
expect(cookie.path).to.be('/');
expect(cookie.httpOnly).to.be(true);
expect(cookie.maxAge).to.be(null);
}
function checkCookieIsCleared(cookie: Cookie) {
expect(cookie.value).to.be.empty();
expect(cookie.key).to.be('sid');
expect(cookie.path).to.be('/');
expect(cookie.httpOnly).to.be(true);
expect(cookie.maxAge).to.be(0);
}
describe('PKI authentication', () => {
before(async () => {
await getService('esSupertest')
.post('/_security/role_mapping/first_client_pki')
.ca(CA_CERT)
.send({
roles: ['kibana_user'],
enabled: true,
rules: { field: { dn: 'CN=first_client' } },
})
.expect(200);
});
it('should reject API requests that use untrusted certificate', async () => {
await supertest
.get('/api/security/v1/me')
.ca(CA_CERT)
.pfx(UNTRUSTED_CLIENT_CERT)
.set('kbn-xsrf', 'xxx')
.expect(401);
});
it('does not prevent basic login', async () => {
const [username, password] = config.get('servers.elasticsearch.auth').split(':');
const response = await supertest
.post('/api/security/v1/login')
.ca(CA_CERT)
.pfx(UNTRUSTED_CLIENT_CERT)
.set('kbn-xsrf', 'xxx')
.send({ username, password })
.expect(204);
const cookies = response.headers['set-cookie'];
expect(cookies).to.have.length(1);
const cookie = request.cookie(cookies[0])!;
checkCookieIsSet(cookie);
const { body: user } = await supertest
.get('/api/security/v1/me')
.ca(CA_CERT)
.pfx(UNTRUSTED_CLIENT_CERT)
.set('kbn-xsrf', 'xxx')
.set('Cookie', cookie.cookieString())
.expect(200);
expect(user.username).to.eql(username);
expect(user.authentication_realm).to.eql({ name: 'reserved', type: 'reserved' });
});
it('should properly set cookie and authenticate user', async () => {
const response = await supertest
.get('/api/security/v1/me')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.expect(200);
const cookies = response.headers['set-cookie'];
expect(cookies).to.have.length(1);
const sessionCookie = request.cookie(cookies[0])!;
checkCookieIsSet(sessionCookie);
expect(response.body).to.eql({
username: 'first_client',
roles: ['kibana_user'],
full_name: null,
email: null,
enabled: true,
metadata: {
pki_delegated_by_realm: 'reserved',
pki_delegated_by_user: 'elastic',
pki_dn: 'CN=first_client',
},
authentication_realm: { name: 'pki1', type: 'pki' },
lookup_realm: { name: 'pki1', type: 'pki' },
});
// Cookie should be accepted.
await supertest
.get('/api/security/v1/me')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.set('Cookie', sessionCookie.cookieString())
.expect(200);
});
it('should update session if new certificate is provided', async () => {
let response = await supertest
.get('/api/security/v1/me')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.expect(200);
const cookies = response.headers['set-cookie'];
expect(cookies).to.have.length(1);
const sessionCookie = request.cookie(cookies[0])!;
checkCookieIsSet(sessionCookie);
response = await supertest
.get('/api/security/v1/me')
.ca(CA_CERT)
.pfx(SECOND_CLIENT_CERT)
.set('Cookie', sessionCookie.cookieString())
.expect(200, {
username: 'second_client',
roles: [],
full_name: null,
email: null,
enabled: true,
metadata: {
pki_delegated_by_realm: 'reserved',
pki_delegated_by_user: 'elastic',
pki_dn: 'CN=second_client',
},
authentication_realm: { name: 'pki1', type: 'pki' },
lookup_realm: { name: 'pki1', type: 'pki' },
});
checkCookieIsSet(request.cookie(response.headers['set-cookie'][0])!);
});
it('should reject valid cookie if used with untrusted certificate', async () => {
const response = await supertest
.get('/api/security/v1/me')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.expect(200);
const cookies = response.headers['set-cookie'];
expect(cookies).to.have.length(1);
const sessionCookie = request.cookie(cookies[0])!;
checkCookieIsSet(sessionCookie);
await supertest
.get('/api/security/v1/me')
.ca(CA_CERT)
.pfx(UNTRUSTED_CLIENT_CERT)
.set('Cookie', sessionCookie.cookieString())
.expect(401);
});
describe('API access with active session', () => {
let sessionCookie: Cookie;
beforeEach(async () => {
const response = await supertest
.get('/api/security/v1/me')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.expect(200);
const cookies = response.headers['set-cookie'];
expect(cookies).to.have.length(1);
sessionCookie = request.cookie(cookies[0])!;
checkCookieIsSet(sessionCookie);
});
it('should extend cookie on every successful non-system API call', async () => {
const apiResponseOne = await supertest
.get('/api/security/v1/me')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.set('kbn-xsrf', 'xxx')
.set('Cookie', sessionCookie.cookieString())
.expect(200);
expect(apiResponseOne.headers['set-cookie']).to.not.be(undefined);
const sessionCookieOne = request.cookie(apiResponseOne.headers['set-cookie'][0])!;
checkCookieIsSet(sessionCookieOne);
expect(sessionCookieOne.value).to.not.equal(sessionCookie.value);
const apiResponseTwo = await supertest
.get('/api/security/v1/me')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.set('kbn-xsrf', 'xxx')
.set('Cookie', sessionCookie.cookieString())
.expect(200);
expect(apiResponseTwo.headers['set-cookie']).to.not.be(undefined);
const sessionCookieTwo = request.cookie(apiResponseTwo.headers['set-cookie'][0])!;
checkCookieIsSet(sessionCookieTwo);
expect(sessionCookieTwo.value).to.not.equal(sessionCookieOne.value);
});
it('should not extend cookie for system API calls', async () => {
const systemAPIResponse = await supertest
.get('/api/security/v1/me')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.set('kbn-xsrf', 'xxx')
.set('kbn-system-api', 'true')
.set('Cookie', sessionCookie.cookieString())
.expect(200);
expect(systemAPIResponse.headers['set-cookie']).to.be(undefined);
});
it('should fail and preserve session cookie if unsupported authentication schema is used', async () => {
const apiResponse = await supertest
.get('/api/security/v1/me')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.set('kbn-xsrf', 'xxx')
.set('Authorization', 'Basic a3JiNTprcmI1')
.set('Cookie', sessionCookie.cookieString())
.expect(401);
expect(apiResponse.headers['set-cookie']).to.be(undefined);
});
});
describe('logging out', () => {
it('should redirect to `logged_out` page after successful logout', async () => {
// First authenticate user to retrieve session cookie.
const response = await supertest
.get('/api/security/v1/me')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.expect(200);
let cookies = response.headers['set-cookie'];
expect(cookies).to.have.length(1);
const sessionCookie = request.cookie(cookies[0])!;
checkCookieIsSet(sessionCookie);
// And then log user out.
const logoutResponse = await supertest
.get('/api/security/v1/logout')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.set('Cookie', sessionCookie.cookieString())
.expect(302);
cookies = logoutResponse.headers['set-cookie'];
expect(cookies).to.have.length(1);
checkCookieIsCleared(request.cookie(cookies[0])!);
expect(logoutResponse.headers.location).to.be('/logged_out');
});
it('should redirect to home page if session cookie is not provided', async () => {
const logoutResponse = await supertest
.get('/api/security/v1/logout')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.expect(302);
expect(logoutResponse.headers['set-cookie']).to.be(undefined);
expect(logoutResponse.headers.location).to.be('/');
});
});
describe('API access with expired access token.', () => {
let sessionCookie: Cookie;
beforeEach(async () => {
const response = await supertest
.get('/api/security/v1/me')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.expect(200);
const cookies = response.headers['set-cookie'];
expect(cookies).to.have.length(1);
sessionCookie = request.cookie(cookies[0])!;
checkCookieIsSet(sessionCookie);
});
it('AJAX call should re-acquire token and update existing cookie', async function() {
this.timeout(40000);
// Access token expiration is set to 15s for API integration tests.
// Let's wait for 20s to make sure token expires.
await delay(20000);
// This api call should succeed and automatically refresh token. Returned cookie will contain
// the new access token.
const apiResponse = await supertest
.get('/api/security/v1/me')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.set('kbn-xsrf', 'xxx')
.set('Cookie', sessionCookie.cookieString())
.expect(200);
const cookies = apiResponse.headers['set-cookie'];
expect(cookies).to.have.length(1);
const refreshedCookie = request.cookie(cookies[0])!;
checkCookieIsSet(refreshedCookie);
});
it('non-AJAX call should re-acquire token and update existing cookie', async function() {
this.timeout(40000);
// Access token expiration is set to 15s for API integration tests.
// Let's wait for 20s to make sure token expires.
await delay(20000);
// This request should succeed and automatically refresh token. Returned cookie will contain
// the new access and refresh token pair.
const nonAjaxResponse = await supertest
.get('/app/kibana')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.set('Cookie', sessionCookie.cookieString())
.expect(200);
const cookies = nonAjaxResponse.headers['set-cookie'];
expect(cookies).to.have.length(1);
const refreshedCookie = request.cookie(cookies[0])!;
checkCookieIsSet(refreshedCookie);
});
});
});
}

View file

@ -0,0 +1,70 @@
/*
* 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 { resolve } from 'path';
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
// @ts-ignore
import { CA_CERT_PATH, ES_KEY_PATH, ES_CERT_PATH } from '@kbn/dev-utils';
import { services } from './services';
export default async function({ readConfigFile }: FtrConfigProviderContext) {
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.js'));
const servers = {
...xPackAPITestsConfig.get('servers'),
elasticsearch: {
...xPackAPITestsConfig.get('servers.elasticsearch'),
protocol: 'https',
},
kibana: {
...xPackAPITestsConfig.get('servers.kibana'),
protocol: 'https',
},
};
return {
testFiles: [require.resolve('./apis')],
servers,
services,
junit: {
reportName: 'X-Pack PKI API Integration Tests',
},
esTestCluster: {
...xPackAPITestsConfig.get('esTestCluster'),
ssl: true,
serverArgs: [
...xPackAPITestsConfig.get('esTestCluster.serverArgs'),
'xpack.security.authc.token.enabled=true',
'xpack.security.authc.token.timeout=15s',
'xpack.security.http.ssl.client_authentication=optional',
'xpack.security.http.ssl.verification_mode=certificate',
'xpack.security.authc.realms.native.native1.order=0',
'xpack.security.authc.realms.pki.pki1.order=1',
'xpack.security.authc.realms.pki.pki1.delegation.enabled=true',
`xpack.security.authc.realms.pki.pki1.certificate_authorities=${CA_CERT_PATH}`,
],
},
kbnTestServer: {
...xPackAPITestsConfig.get('kbnTestServer'),
serverArgs: [
...xPackAPITestsConfig.get('kbnTestServer.serverArgs'),
'--server.ssl.enabled=true',
`--server.ssl.key=${ES_KEY_PATH}`,
`--server.ssl.certificate=${ES_CERT_PATH}`,
`--server.ssl.certificateAuthorities=${JSON.stringify([
CA_CERT_PATH,
resolve(__dirname, './fixtures/kibana_ca.crt'),
])}`,
`--server.ssl.clientAuthentication=required`,
`--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`,
`--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`,
`--xpack.security.authc.providers=${JSON.stringify(['pki', 'basic'])}`,
],
},
};
}

View file

@ -0,0 +1,7 @@
# PKI Fixtures
* `es_ca.key` - the CA key used to sign certificates from @kbn/dev-utils that are used and trusted by test Elasticsearch server.
* `first_client.p12` and `second_client.p12` - the client certificate bundles signed by `es_ca.key` and hence trusted by
both test Kibana and Elasticsearch servers.
* `untrusted_client.p12` - the client certificate bundle trusted by test Kibana server, but not test Elasticsearch test server.
* `kibana_ca.crt` and `kibana_ca.key` - the CA certificate and key trusted by test Kibana server only.

View file

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAjSJiqfwPZfvgHO1OZbxzgPn2EW/KewIHXygTAdL926Pm6R45
G5H972B46NcSUoOZbOhDyvg6OKMJAICiXa85yOf3nyTo4APspR+K4AH60SEJohRF
mZwL/OryfiKvN5n5DxC2+Hb1wouwBUJM6DP62C24ve8YWuWwNkhJqWKe1YQUzPc1
svqvU5uaHTzvLtp++RqSDNkcIqWl5S9Ip5PtOv6MHkCaIr2g4KQzplFwhT5qVd1Q
nYVBsQ0D8htLqUJBfjW0KHouEZpbjxJlc+EuyExS1o1+y3mVT+t2yZHAoIquh5ve
5A7a/RGJTyoR5u1DFs4Tcx2378kjA86gCQtClwIDAQABAoIBAFTOGKMzxrztQJmh
Lr6LIoyZpnaLygtoCK3xEprCAbB9KD9j3cTnUMMKIR0oPuY+FW8Pkczgo3ts2/fl
U6sfo4VJfc2vDA+vy/7cmUJJbkFDrNorfDb1QW7UbqnEhazPZIzc6lUahkpETZyb
XkMZGN3Ve3EFvojAA8ZaYYjarb52HRddLPZJ7c8ZiHfJ1jHNIvx6dIQ6CJVuovBJ
OGbbSAK8MjUtOI2XzWNHgUqGHcjVDFysuAac3ckK14TaN4KVNRl+usAMkZwqSM5u
j/ATFL9hx7nkzh3KWPsuOLMoLX7JN81z0YtT52wTxJoSiZKk/u91JHZ3NcrsOSPS
oLvVkyECgYEA16qtXvtmboAbqeuXf0nF+7QD0b+MdaRFIacqTG0LpEgY9Tjgs9Pn
6z44tHABWPVkRLNQZiky99MAq4Ci354Bk9dmylCw9ADH78VGmKWklbQEr1rw4dqm
DHTj9NQ79SyTdiasQjnnxCilWkrO6ZUqD8og4DT5MhzfxO/ZND8arGsCgYEAp4df
oI5lwlc1n9X/G9RQAKwNM5un8RmReleUVemjkcvWwvZVEjV0Gcc1WtjB+77Y5B9B
CM3laURDGrAgX5VS/I2jb0xqBNUr8XccSkDQAP9UuVPZgxpS+8d0R3fxVzniHWwR
WC2dW/Is40i/6+7AkFXhkiFiqxkvSg4pWHPazYUCgYB/gP7C+urSRZcVXJ3SuXD9
oK3pYc/O9XGRtd0CFi4d0CpBQIFIj+27XKv1sYp6Z4oCO+k6nPzvG6Z3vrOMdUQF
fgHddttHRvbtwLo+ISAvCaEDc0aaoMQu9SSYaKmSB+qenbqV5NorVMR9n2C5JGEb
uKq7I1Z41C1Pp2XIx84jRQKBgQCjKvfZsjesZDJnfg9dtJlDPlARXt7gte16gkiI
sOnOfAGtnCzZclSlMuBlnk65enVXIpW+FIQH1iOhn7+4OQE92FpBceSk1ldZdJCK
RbwR7J5Bb0igJ4iBkA9R+KGIOmlgDLyL7MmiHyrXKCk9iynkqrDsGjY2vW3QrCBa
9WQ73QKBgQDAYZzplO4TPoPK9AnxoW/HpSwGEO7Fb8fLyPg94CvHn4QBCFJUKuTn
hBp/TJgF6CjQWQMr2FKVFF33Ow7+Qa96YGvmYlEjR/71D4Rlprj5JJpuO154DI3I
YIMNTjvwEQEI+YamMarKsz0Kq+I1EYSAf6bQ4H2PgxDxwTXaLkl0RA==
-----END RSA PRIVATE KEY-----

View file

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDCjCCAfKgAwIBAgIVAK8/CDsQxdAItvVPu2P72xXx4pbAMA0GCSqGSIb3DQEB
CwUAMBQxEjAQBgNVBAMTCUtpYmFuYSBDQTAeFw0xOTA4MTQxNTAwNDFaFw0yMjA4
MTMxNTAwNDFaMBQxEjAQBgNVBAMTCUtpYmFuYSBDQTCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBANDBPAHZvBBtOZ/9aBHVmBFA3QS35wemnT2VwFE6LSUw
35Tj3/Vj/1NQAqAqKOUTCE0zQAyDBOGWAa1MadhYC2Fvxt/VUoOJWczeMuO3ktua
ybk3xzJJcOSoPjbPBUfQuRQ7GnBJsjyHKgPXIsP6wshQosYZnHPJcZSF1+6N9aGJ
psV/ukdLD8oJFq3pv7D9KY/gbAFeVkwWwdx9dqtfT0STGXOOZnLAz8ZmWH2WIt+f
t7+9EIv1pIUM6KOqANmhxxyitvka7XdN/ZEnwV/+Is9y/6N0NGaC9BWWoCNAgvuX
Ep0R+5qvNtCkL8okLaCc0a/B843e3k7eWuI8ES3Dhg0CAwEAAaNTMFEwHQYDVR0O
BBYEFBEp58Oz7rIAbT5O/yOGnSQcasG7MB8GA1UdIwQYMBaAFBEp58Oz7rIAbT5O
/yOGnSQcasG7MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAKZ/
ZblM7pEOP77DLePM3NpjJQu73a7vjou2n0ifEq0HYsSMuKverZhhrc4L2PjRM34A
NVtcSsjnc2OkhtG6baV8q/GyDtvUXwnfCnI2MxNiVtmX7fWzHZVwkd4GCXnvOd3S
IBxzh4OYLV2rTFjo7oUWdDV+nFGVzQhhdlQ/fZ8by6g0qZvKKfe70Z3prmkRRRxz
QslJYQwB+cK3rdyAVJDYGbMGcJjM50PR3iM/PqQFAwOcyW9th1CpiHOOmbcQRmCS
W7h8A2TDzqvFWOz0QRoldt93vCXkP6PF3UXo2wpSPt8tzd6e0z0+HIyhYGUPstiE
zO36/AJiPQicgQK60gI=
-----END CERTIFICATE-----

View file

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA0ME8Adm8EG05n/1oEdWYEUDdBLfnB6adPZXAUTotJTDflOPf
9WP/U1ACoCoo5RMITTNADIME4ZYBrUxp2FgLYW/G39VSg4lZzN4y47eS25rJuTfH
Mklw5Kg+Ns8FR9C5FDsacEmyPIcqA9ciw/rCyFCixhmcc8lxlIXX7o31oYmmxX+6
R0sPygkWrem/sP0pj+BsAV5WTBbB3H12q19PRJMZc45mcsDPxmZYfZYi35+3v70Q
i/WkhQzoo6oA2aHHHKK2+Rrtd039kSfBX/4iz3L/o3Q0ZoL0FZagI0CC+5cSnRH7
mq820KQvyiQtoJzRr8Hzjd7eTt5a4jwRLcOGDQIDAQABAoIBACZm5bsRat86uJcN
7s8ZE9hYrk/n5MArjlF98tr+cL+etgKVyOVDd/zDgzgjiVJapfRNsUKb95HoHnba
z73UtINAJL2YaI15/uMJHSN26bUsTF+eOy6tA++MY6WBf98uLl3iYYK2i+tGkhwS
v3p97scazlbS70z9ib9gv9BKnR0R+DTDBACwNtfurOGQh9PDU/e3orsrVBR/kj7o
nfjlXZzsuuVdGRHmO2yGoALCx1N0dMpO/ALWDi+phP5Jz6SBo0AGAKfC2tXZJrVz
qwHfCPnklIphHHmFkArmrAYZDOHBNtLRFQL8SmfcZOz3HO9er87ct/zAHN5im91s
vVZnYQECgYEA90u/q4Ux9A2iaJ4qfDqjM8i+DLezj0ogLLe9OZuBgKfy41bt7ilX
4iQ6cmmzq/9x0dM2ydRXTVJ0Ek/0EDgfVxxcWTRSHHrwy+WOnaB0hWkMzwgDak58
fhRi5RAhXhCJlUyHsU7FBTYcwQE6I/C971X0AuCH06eEeaHG8HOERzkCgYEA2Bo2
cYvRmfL7uh4STWTJkq08ppNDgo1aJ8brc+YtK3v11OZEbnSSmq0rdnDqkvFBmSrJ
wHprhs+RGBKBJzI3BMZ9uxOzhufk/xPZrpZie77JFvpzMSbC0WMTpb06QsGk1bu7
jqtbNx8OYYF7PVxiuUsZY3sOIZ5b3t9yWeiFwXUCgYB8tcyRGPiaFQ4kKC9Qutl2
0fNVwoZg6obTRk288XkbgpbwovQWOO9C8fYvoLKlOIsTv6pPmi/0pHI4ke2JCGR1
r626prIJ/s3UZY3IXBSm+tUkyuu9/pq1kl5VGg9ZuolHq3J6rjiZajKR+qZxXYTL
X9NQaB7XVBFwrW7/76FzsQKBgCOOVIzkI22AFDjwP7SqM5xFkqgZrM7rMP1AdncQ
VThFYhJQfMvrtD9s5KzNMVtSBKgN6ToZKl35AveB++wWEAViH0fLmwtEVmI9wuA9
8CBKKM32EUPyC7Xl5lKrys03DUb5Z4e23AA6xOP4KO3UqI2yNJAwrAeOBbGq9Cak
4nUNAoGBAKBhG767ePf/5fMrKJnsdxK2+fpP1UglHHTKOxwtLU9xtkUw96g7yzxj
5ma66yv1QuGIvAmbLVLCI6MmFvXYQmomoRt2KnYEgxjg8jjXXDdoOsNL87t7l0HA
CviL/UR7ZZV28rp5TmexRworu7hC4qer9NZxqV0a7bOLq0+uVda9
-----END RSA PRIVATE KEY-----

View file

@ -0,0 +1,11 @@
/*
* 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 { GenericFtrProviderContext } from '@kbn/test/types/ftr';
import { services } from './services';
export type FtrProviderContext = GenericFtrProviderContext<typeof services, {}>;

View file

@ -0,0 +1,12 @@
/*
* 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 { services as apiIntegrationServices } from '../api_integration/services';
export const services = {
esSupertest: apiIntegrationServices.esSupertest,
supertestWithoutAuth: apiIntegrationServices.supertestWithoutAuth,
};