From f5a238f7b0aca54fff0178942494ce5488701745 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Tue, 27 Aug 2019 18:28:54 +0200 Subject: [PATCH 1/2] Introduce PKI authentication provider. (#42606) --- src/core/server/http/http_server.mocks.ts | 5 +- .../legacy/server/lib/esjs_shield_plugin.js | 16 + .../server/authentication/authenticator.ts | 2 + .../authentication/providers/base.mock.ts | 23 +- .../server/authentication/providers/index.ts | 1 + .../authentication/providers/pki.test.ts | 589 ++++++++++++++++++ .../server/authentication/providers/pki.ts | 277 ++++++++ x-pack/scripts/functional_tests.js | 1 + x-pack/test/pki_api_integration/apis/index.ts | 14 + .../apis/security/index.ts | 13 + .../apis/security/pki_auth.ts | 370 +++++++++++ x-pack/test/pki_api_integration/config.ts | 70 +++ .../pki_api_integration/fixtures/README.md | 7 + .../pki_api_integration/fixtures/es_ca.key | 27 + .../fixtures/first_client.p12 | Bin 0 -> 3467 bytes .../fixtures/kibana_ca.crt | 19 + .../fixtures/kibana_ca.key | 27 + .../fixtures/second_client.p12 | Bin 0 -> 3469 bytes .../fixtures/untrusted_client.p12 | Bin 0 -> 3387 bytes .../ftr_provider_context.d.ts | 11 + x-pack/test/pki_api_integration/services.ts | 12 + 21 files changed, 1482 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/security/server/authentication/providers/pki.test.ts create mode 100644 x-pack/plugins/security/server/authentication/providers/pki.ts create mode 100644 x-pack/test/pki_api_integration/apis/index.ts create mode 100644 x-pack/test/pki_api_integration/apis/security/index.ts create mode 100644 x-pack/test/pki_api_integration/apis/security/pki_auth.ts create mode 100644 x-pack/test/pki_api_integration/config.ts create mode 100644 x-pack/test/pki_api_integration/fixtures/README.md create mode 100644 x-pack/test/pki_api_integration/fixtures/es_ca.key create mode 100644 x-pack/test/pki_api_integration/fixtures/first_client.p12 create mode 100644 x-pack/test/pki_api_integration/fixtures/kibana_ca.crt create mode 100644 x-pack/test/pki_api_integration/fixtures/kibana_ca.key create mode 100644 x-pack/test/pki_api_integration/fixtures/second_client.p12 create mode 100644 x-pack/test/pki_api_integration/fixtures/untrusted_client.p12 create mode 100644 x-pack/test/pki_api_integration/ftr_provider_context.d.ts create mode 100644 x-pack/test/pki_api_integration/services.ts diff --git a/src/core/server/http/http_server.mocks.ts b/src/core/server/http/http_server.mocks.ts index 33a98127aa630..fcc232345a802 100644 --- a/src/core/server/http/http_server.mocks.ts +++ b/src/core/server/http/http_server.mocks.ts @@ -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; 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, { diff --git a/x-pack/legacy/server/lib/esjs_shield_plugin.js b/x-pack/legacy/server/lib/esjs_shield_plugin.js index ced6bd0d889ed..33cd53dc9d5d3 100644 --- a/x-pack/legacy/server/lib/esjs_shield_plugin.js +++ b/x-pack/legacy/server/lib/esjs_shield_plugin.js @@ -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', + }, + }); }; })); diff --git a/x-pack/plugins/security/server/authentication/authenticator.ts b/x-pack/plugins/security/server/authentication/authenticator.ts index c4dcd82d7ff9d..720bfb4c6b1e3 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.ts @@ -25,6 +25,7 @@ import { SAMLAuthenticationProvider, TokenAuthenticationProvider, OIDCAuthenticationProvider, + PKIAuthenticationProvider, isSAMLRequestQuery, } from './providers'; import { AuthenticationResult } from './authentication_result'; @@ -99,6 +100,7 @@ const providerMap = new Map< ['saml', SAMLAuthenticationProvider], ['token', TokenAuthenticationProvider], ['oidc', OIDCAuthenticationProvider], + ['pki', PKIAuthenticationProvider], ]); function assertRequest(request: KibanaRequest) { diff --git a/x-pack/plugins/security/server/authentication/providers/base.mock.ts b/x-pack/plugins/security/server/authentication/providers/base.mock.ts index e45bb3b090542..c88c72a8e696a 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.mock.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.mock.ts @@ -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 @@ -35,3 +43,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() }, + }; +} diff --git a/x-pack/plugins/security/server/authentication/providers/index.ts b/x-pack/plugins/security/server/authentication/providers/index.ts index fef3be16c8d91..af0b90766e859 100644 --- a/x-pack/plugins/security/server/authentication/providers/index.ts +++ b/x-pack/plugins/security/server/authentication/providers/index.ts @@ -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'; diff --git a/x-pack/plugins/security/server/authentication/providers/pki.test.ts b/x-pack/plugins/security/server/authentication/providers/pki.test.ts new file mode 100644 index 0000000000000..35d827c3a9bd1 --- /dev/null +++ b/x-pack/plugins/security/server/authentication/providers/pki.test.ts @@ -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 { + 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 + ); + + 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 + ); + 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 + ); + 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 + ); + 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 + ); + 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 + ); + + 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 + ); + 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 + ); + + 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 + ); + + 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 + ); + + 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 + ); + + 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 + ); + + 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'); + }); + }); +}); diff --git a/x-pack/plugins/security/server/authentication/providers/pki.ts b/x-pack/plugins/security/server/authentication/providers/pki.ts new file mode 100644 index 0000000000000..788395feae442 --- /dev/null +++ b/x-pack/plugins/security/server/authentication/providers/pki.ts @@ -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; + } +} diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 222647e000672..f513d117e08ae 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -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'), diff --git a/x-pack/test/pki_api_integration/apis/index.ts b/x-pack/test/pki_api_integration/apis/index.ts new file mode 100644 index 0000000000000..d859ed172ac69 --- /dev/null +++ b/x-pack/test/pki_api_integration/apis/index.ts @@ -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')); + }); +} diff --git a/x-pack/test/pki_api_integration/apis/security/index.ts b/x-pack/test/pki_api_integration/apis/security/index.ts new file mode 100644 index 0000000000000..d2bfe613ca7fa --- /dev/null +++ b/x-pack/test/pki_api_integration/apis/security/index.ts @@ -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')); + }); +} diff --git a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts new file mode 100644 index 0000000000000..8c29db674aaf3 --- /dev/null +++ b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts @@ -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); + }); + }); + }); +} diff --git a/x-pack/test/pki_api_integration/config.ts b/x-pack/test/pki_api_integration/config.ts new file mode 100644 index 0000000000000..50b41ad251827 --- /dev/null +++ b/x-pack/test/pki_api_integration/config.ts @@ -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'])}`, + ], + }, + }; +} diff --git a/x-pack/test/pki_api_integration/fixtures/README.md b/x-pack/test/pki_api_integration/fixtures/README.md new file mode 100644 index 0000000000000..0fcbc76183b48 --- /dev/null +++ b/x-pack/test/pki_api_integration/fixtures/README.md @@ -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. \ No newline at end of file diff --git a/x-pack/test/pki_api_integration/fixtures/es_ca.key b/x-pack/test/pki_api_integration/fixtures/es_ca.key new file mode 100644 index 0000000000000..5428f86851e5a --- /dev/null +++ b/x-pack/test/pki_api_integration/fixtures/es_ca.key @@ -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----- diff --git a/x-pack/test/pki_api_integration/fixtures/first_client.p12 b/x-pack/test/pki_api_integration/fixtures/first_client.p12 new file mode 100644 index 0000000000000000000000000000000000000000..62da80d9ab80efbedd24edf322acbc1aecc38ef8 GIT binary patch literal 3467 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 diff --git a/x-pack/test/pki_api_integration/services.ts b/x-pack/test/pki_api_integration/services.ts new file mode 100644 index 0000000000000..31f58df081ddb --- /dev/null +++ b/x-pack/test/pki_api_integration/services.ts @@ -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, +}; From 1e243519522aab509cb5845d396462ac106075e1 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Tue, 27 Aug 2019 19:36:06 +0200 Subject: [PATCH 2/2] Adapt auth mock to 7.x. --- .../security/server/authentication/providers/base.mock.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/security/server/authentication/providers/base.mock.ts b/x-pack/plugins/security/server/authentication/providers/base.mock.ts index c88c72a8e696a..07ac7b40935ec 100644 --- a/x-pack/plugins/security/server/authentication/providers/base.mock.ts +++ b/x-pack/plugins/security/server/authentication/providers/base.mock.ts @@ -50,6 +50,7 @@ export function mockAuthenticationProviderOptionsWithJest() { basePath.get.mockReturnValue('/base-path'); return { + getServerBaseURL: () => 'test-protocol://test-hostname:1234', client: elasticsearchServiceMock.createClusterClient(), logger: loggingServiceMock.create().get(), basePath,