Prevent Kerberos and PKI providers from initiating a new session for unauthenticated XHR/API requests. (#82817)

* Prevent Kerberos and PKI providers from initiating a new session for unauthenticated XHR requests.

* Review#1: fix comment.
This commit is contained in:
Aleh Zasypkin 2020-11-09 17:34:20 +01:00 committed by GitHub
parent 55cf3bd0a6
commit 45ddd69ca2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 271 additions and 290 deletions

3
.github/CODEOWNERS vendored
View file

@ -254,9 +254,6 @@
/x-pack/test/ui_capabilities/ @elastic/kibana-security
/x-pack/test/encrypted_saved_objects_api_integration/ @elastic/kibana-security
/x-pack/test/functional/apps/security/ @elastic/kibana-security
/x-pack/test/kerberos_api_integration/ @elastic/kibana-security
/x-pack/test/oidc_api_integration/ @elastic/kibana-security
/x-pack/test/pki_api_integration/ @elastic/kibana-security
/x-pack/test/security_api_integration/ @elastic/kibana-security
/x-pack/test/security_functional/ @elastic/kibana-security
/x-pack/test/spaces_api_integration/ @elastic/kibana-security

View file

@ -346,6 +346,16 @@ describe('KerberosAuthenticationProvider', () => {
expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled();
});
it('does not start SPNEGO for Ajax requests.', async () => {
const request = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } });
await expect(provider.authenticate(request)).resolves.toEqual(
AuthenticationResult.notHandled()
);
expect(mockOptions.client.asScoped).not.toHaveBeenCalled();
expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled();
});
it('succeeds if state contains a valid token.', async () => {
const user = mockAuthenticatedUser();
const request = httpServerMock.createKibanaRequest({ headers: {} });
@ -442,9 +452,6 @@ describe('KerberosAuthenticationProvider', () => {
});
it('fails with `Negotiate` challenge if both access and refresh tokens from the state are expired and backend supports Kerberos.', async () => {
const request = httpServerMock.createKibanaRequest();
const tokenPair = { accessToken: 'expired-token', refreshToken: 'some-valid-refresh-token' };
const failureReason = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(
new (errors.AuthenticationException as any)('Unauthorized', {
body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } },
@ -456,37 +463,45 @@ describe('KerberosAuthenticationProvider', () => {
mockOptions.tokens.refresh.mockResolvedValue(null);
await expect(provider.authenticate(request, tokenPair)).resolves.toEqual(
const nonAjaxRequest = httpServerMock.createKibanaRequest();
const nonAjaxTokenPair = {
accessToken: 'expired-token',
refreshToken: 'some-valid-refresh-token',
};
await expect(provider.authenticate(nonAjaxRequest, nonAjaxTokenPair)).resolves.toEqual(
AuthenticationResult.failed(failureReason, {
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
})
);
expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1);
expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken);
});
it('does not re-start SPNEGO if both access and refresh tokens from the state are expired.', async () => {
const request = httpServerMock.createKibanaRequest({ routeAuthRequired: false });
const tokenPair = { accessToken: 'expired-token', refreshToken: 'some-valid-refresh-token' };
const failureReason = LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(
new (errors.AuthenticationException as any)('Unauthorized', {
body: { error: { header: { 'WWW-Authenticate': 'Negotiate' } } },
const ajaxRequest = httpServerMock.createKibanaRequest({ headers: { 'kbn-xsrf': 'xsrf' } });
const ajaxTokenPair = {
accessToken: 'expired-token',
refreshToken: 'ajax-some-valid-refresh-token',
};
await expect(provider.authenticate(ajaxRequest, ajaxTokenPair)).resolves.toEqual(
AuthenticationResult.failed(failureReason, {
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
})
);
const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockRejectedValue(failureReason);
mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient);
mockOptions.tokens.refresh.mockResolvedValue(null);
await expect(provider.authenticate(request, tokenPair)).resolves.toEqual(
AuthenticationResult.notHandled()
const optionalAuthRequest = httpServerMock.createKibanaRequest({ routeAuthRequired: false });
const optionalAuthTokenPair = {
accessToken: 'expired-token',
refreshToken: 'optional-some-valid-refresh-token',
};
await expect(
provider.authenticate(optionalAuthRequest, optionalAuthTokenPair)
).resolves.toEqual(
AuthenticationResult.failed(failureReason, {
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
})
);
expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(1);
expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(tokenPair.refreshToken);
expect(mockOptions.tokens.refresh).toHaveBeenCalledTimes(3);
expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(nonAjaxTokenPair.refreshToken);
expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(ajaxTokenPair.refreshToken);
expect(mockOptions.tokens.refresh).toHaveBeenCalledWith(optionalAuthTokenPair.refreshToken);
});
});

View file

@ -13,6 +13,7 @@ import {
import { AuthenticationResult } from '../authentication_result';
import { DeauthenticationResult } from '../deauthentication_result';
import { HTTPAuthorizationHeader } from '../http_authentication';
import { canRedirectRequest } from '../can_redirect_request';
import { Tokens, TokenPair } from '../tokens';
import { BaseAuthenticationProvider } from './base';
@ -32,8 +33,9 @@ const WWWAuthenticateHeaderName = 'WWW-Authenticate';
* @param request Request instance.
*/
function canStartNewSession(request: KibanaRequest) {
// We should try to establish new session only if request requires authentication.
return request.route.options.authRequired === true;
// We should try to establish new session only if request requires authentication and it's not an XHR request.
// Technically we can authenticate XHR requests too, but we don't want these to create a new session unintentionally.
return canRedirectRequest(request) && request.route.options.authRequired === true;
}
/**
@ -75,11 +77,8 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider {
return AuthenticationResult.notHandled();
}
let authenticationResult = authorizationHeader
? await this.authenticateWithNegotiateScheme(request)
: AuthenticationResult.notHandled();
if (state && authenticationResult.notHandled()) {
let authenticationResult = AuthenticationResult.notHandled();
if (state) {
authenticationResult = await this.authenticateViaState(request, state);
if (
authenticationResult.failed() &&
@ -89,11 +88,15 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider {
}
}
// If we couldn't authenticate by means of all methods above, let's try to check if Elasticsearch can
// start authentication mechanism negotiation, otherwise just return authentication result we have.
return authenticationResult.notHandled() && canStartNewSession(request)
? await this.authenticateViaSPNEGO(request, state)
: authenticationResult;
if (!authenticationResult.notHandled() || !canStartNewSession(request)) {
return authenticationResult;
}
// If we couldn't authenticate by means of all methods above, let's check if we're already at the authentication
// mechanism negotiation stage, otherwise check with Elasticsearch if we can start it.
return authorizationHeader
? await this.authenticateWithNegotiateScheme(request)
: await this.authenticateViaSPNEGO(request, state);
}
/**
@ -264,12 +267,12 @@ export class KerberosAuthenticationProvider extends BaseAuthenticationProvider {
return AuthenticationResult.failed(err);
}
// If refresh token is no longer valid, then we should clear session and renegotiate using SPNEGO.
// If refresh token is no longer valid, let's try to renegotiate new tokens using SPNEGO. We
// allow this because expired underlying token is an implementation detail and Kibana user
// facing session is still valid.
if (refreshedTokenPair === null) {
this.logger.debug('Both access and refresh tokens are expired.');
return canStartNewSession(request)
? this.authenticateViaSPNEGO(request, state)
: AuthenticationResult.notHandled();
this.logger.debug('Both access and refresh tokens are expired. Re-authenticating...');
return this.authenticateViaSPNEGO(request, state);
}
try {

View file

@ -295,6 +295,22 @@ describe('PKIAuthenticationProvider', () => {
expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled();
});
it('does not exchange peer certificate to access token for Ajax requests.', async () => {
const request = httpServerMock.createKibanaRequest({
headers: { 'kbn-xsrf': 'xsrf' },
socket: getMockSocket({
authorized: true,
peerCertificate: getMockPeerCertificate(['2A:7A:C2:DD', '3B:8B:D3:EE']),
}),
});
await expect(provider.authenticate(request)).resolves.toEqual(
AuthenticationResult.notHandled()
);
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 }),
@ -383,14 +399,7 @@ describe('PKIAuthenticationProvider', () => {
});
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 user = mockAuthenticatedUser({ authentication_provider: { type: 'pki', name: 'pki' } });
const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser
@ -399,21 +408,74 @@ describe('PKIAuthenticationProvider', () => {
LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())
)
// In response to a call with a new token.
.mockResolvedValueOnce(user) // In response to call with an expired token.
.mockRejectedValueOnce(
LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())
)
// In response to a call with a new token.
.mockResolvedValueOnce(user) // In response to call with an expired token.
.mockRejectedValueOnce(
LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())
)
// In response to a call with a new token.
.mockResolvedValueOnce(user);
mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient);
mockOptions.client.callAsInternalUser.mockResolvedValue({ access_token: 'access-token' });
await expect(provider.authenticate(request, state)).resolves.toEqual(
AuthenticationResult.succeeded(
{ ...user, authentication_provider: { type: 'pki', name: 'pki' } },
{
authHeaders: { authorization: 'Bearer access-token' },
state: { accessToken: 'access-token', peerCertificateFingerprint256: '2A:7A:C2:DD' },
}
)
const nonAjaxRequest = httpServerMock.createKibanaRequest({
socket: getMockSocket({
authorized: true,
peerCertificate: getMockPeerCertificate(['2A:7A:C2:DD', '3B:8B:D3:EE']),
}),
});
const nonAjaxState = {
accessToken: 'existing-token',
peerCertificateFingerprint256: '2A:7A:C2:DD',
};
await expect(provider.authenticate(nonAjaxRequest, nonAjaxState)).resolves.toEqual(
AuthenticationResult.succeeded(user, {
authHeaders: { authorization: 'Bearer access-token' },
state: { accessToken: 'access-token', peerCertificateFingerprint256: '2A:7A:C2:DD' },
})
);
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(1);
const ajaxRequest = httpServerMock.createKibanaRequest({
headers: { 'kbn-xsrf': 'xsrf' },
socket: getMockSocket({
authorized: true,
peerCertificate: getMockPeerCertificate(['3A:7A:C2:DD', '3B:8B:D3:EE']),
}),
});
const ajaxState = {
accessToken: 'existing-token',
peerCertificateFingerprint256: '3A:7A:C2:DD',
};
await expect(provider.authenticate(ajaxRequest, ajaxState)).resolves.toEqual(
AuthenticationResult.succeeded(user, {
authHeaders: { authorization: 'Bearer access-token' },
state: { accessToken: 'access-token', peerCertificateFingerprint256: '3A:7A:C2:DD' },
})
);
const optionalAuthRequest = httpServerMock.createKibanaRequest({
routeAuthRequired: false,
socket: getMockSocket({
authorized: true,
peerCertificate: getMockPeerCertificate(['4A:7A:C2:DD', '3B:8B:D3:EE']),
}),
});
const optionalAuthState = {
accessToken: 'existing-token',
peerCertificateFingerprint256: '4A:7A:C2:DD',
};
await expect(provider.authenticate(optionalAuthRequest, optionalAuthState)).resolves.toEqual(
AuthenticationResult.succeeded(user, {
authHeaders: { authorization: 'Bearer access-token' },
state: { accessToken: 'access-token', peerCertificateFingerprint256: '4A:7A:C2:DD' },
})
);
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledTimes(3);
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', {
body: {
x509_certificate_chain: [
@ -422,32 +484,26 @@ describe('PKIAuthenticationProvider', () => {
],
},
});
expect(request.headers).not.toHaveProperty('authorization');
});
it('does not exchange peer certificate to a new access token even if existing token is expired and request does not require authentication.', async () => {
const request = httpServerMock.createKibanaRequest({
routeAuthRequired: false,
socket: getMockSocket({
authorized: true,
peerCertificate: getMockPeerCertificate(['2A:7A:C2:DD', '3B:8B:D3:EE']),
}),
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', {
body: {
x509_certificate_chain: [
'fingerprint:3A:7A:C2:DD:base64',
'fingerprint:3B:8B:D3:EE:base64',
],
},
});
expect(mockOptions.client.callAsInternalUser).toHaveBeenCalledWith('shield.delegatePKI', {
body: {
x509_certificate_chain: [
'fingerprint:4A:7A:C2:DD:base64',
'fingerprint:3B:8B:D3:EE:base64',
],
},
});
const state = { accessToken: 'existing-token', peerCertificateFingerprint256: '2A:7A:C2:DD' };
const mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient();
mockScopedClusterClient.callAsCurrentUser.mockRejectedValueOnce(
LegacyElasticsearchErrorHelpers.decorateNotAuthorizedError(new Error())
);
mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient);
await expect(provider.authenticate(request, state)).resolves.toEqual(
AuthenticationResult.notHandled()
);
expect(mockOptions.client.callAsInternalUser).not.toHaveBeenCalled();
expect(request.headers).not.toHaveProperty('authorization');
expect(nonAjaxRequest.headers).not.toHaveProperty('authorization');
expect(ajaxRequest.headers).not.toHaveProperty('authorization');
expect(optionalAuthRequest.headers).not.toHaveProperty('authorization');
});
it('fails with 401 if existing token is expired, but certificate is not present.', async () => {

View file

@ -10,6 +10,7 @@ import { KibanaRequest } from '../../../../../../src/core/server';
import { AuthenticationResult } from '../authentication_result';
import { DeauthenticationResult } from '../deauthentication_result';
import { HTTPAuthorizationHeader } from '../http_authentication';
import { canRedirectRequest } from '../can_redirect_request';
import { Tokens } from '../tokens';
import { BaseAuthenticationProvider } from './base';
@ -33,8 +34,9 @@ interface ProviderState {
* @param request Request instance.
*/
function canStartNewSession(request: KibanaRequest) {
// We should try to establish new session only if request requires authentication.
return request.route.options.authRequired === true;
// We should try to establish new session only if request requires authentication and it's not an XHR request.
// Technically we can authenticate XHR requests too, but we don't want these to create a new session unintentionally.
return canRedirectRequest(request) && request.route.options.authRequired === true;
}
/**
@ -75,12 +77,14 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider {
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 assuming request can initiate new session.
// a new one in exchange to peer certificate chain. Since we know that we had a valid session
// before we can safely assume that it's desired to automatically re-create session even for XHR
// requests.
const invalidAccessToken =
authenticationResult.notHandled() ||
(authenticationResult.failed() &&
Tokens.isAccessTokenExpiredError(authenticationResult.error));
if (invalidAccessToken && canStartNewSession(request)) {
if (invalidAccessToken) {
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
@ -88,14 +92,12 @@ export class PKIAuthenticationProvider extends BaseAuthenticationProvider {
if (authenticationResult.notHandled()) {
return AuthenticationResult.failed(Boom.unauthorized());
}
} else if (invalidAccessToken) {
return AuthenticationResult.notHandled();
}
}
// 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.
// We shouldn't establish new session if authentication isn't required for this particular request.
// If we couldn't authenticate by means of all methods above, let's check if the request is allowed
// to start a new session, and if so try to authenticate request using its peer certificate chain,
// otherwise just return authentication result we have.
return authenticationResult.notHandled() && canStartNewSession(request)
? await this.authenticateViaPeerCertificate(request)
: authenticationResult;

View file

@ -32,19 +32,19 @@ const onlyNotInCoverageTests = [
require.resolve('../test/detection_engine_api_integration/basic/config.ts'),
require.resolve('../test/lists_api_integration/security_and_spaces/config.ts'),
require.resolve('../test/plugin_api_integration/config.ts'),
require.resolve('../test/kerberos_api_integration/config.ts'),
require.resolve('../test/kerberos_api_integration/anonymous_access.config.ts'),
require.resolve('../test/security_api_integration/saml.config.ts'),
require.resolve('../test/security_api_integration/session_idle.config.ts'),
require.resolve('../test/security_api_integration/session_lifespan.config.ts'),
require.resolve('../test/security_api_integration/login_selector.config.ts'),
require.resolve('../test/security_api_integration/audit.config.ts'),
require.resolve('../test/security_api_integration/kerberos.config.ts'),
require.resolve('../test/security_api_integration/kerberos_anonymous_access.config.ts'),
require.resolve('../test/security_api_integration/pki.config.ts'),
require.resolve('../test/security_api_integration/oidc.config.ts'),
require.resolve('../test/security_api_integration/oidc_implicit_flow.config.ts'),
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/observability_api_integration/basic/config.ts'),
require.resolve('../test/observability_api_integration/trial/config.ts'),
require.resolve('../test/pki_api_integration/config.ts'),
require.resolve('../test/encrypted_saved_objects_api_integration/config.ts'),
require.resolve('../test/spaces_api_integration/spaces_only/config.ts'),
require.resolve('../test/spaces_api_integration/security_and_spaces/config_trial.ts'),

View file

@ -1,14 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('apis Kerberos', function () {
this.tags('ciGroup6');
loadTestFile(require.resolve('./security'));
});
}

View file

@ -1,11 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { GenericFtrProviderContext } from '@kbn/test/types/ftr';
import { services } from './services';
export type FtrProviderContext = GenericFtrProviderContext<typeof services, {}>;

View file

@ -1,15 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { services as commonServices } from '../common/services';
import { services as apiIntegrationServices } from '../api_integration/services';
export const services = {
...commonServices,
legacyEs: apiIntegrationServices.legacyEs,
esSupertest: apiIntegrationServices.esSupertest,
supertestWithoutAuth: apiIntegrationServices.supertestWithoutAuth,
};

View file

@ -1,11 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { GenericFtrProviderContext } from '@kbn/test/types/ftr';
import { services } from './services';
export type FtrProviderContext = GenericFtrProviderContext<typeof services, {}>;

View file

@ -1,14 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { services as commonServices } from '../common/services';
import { services as apiIntegrationServices } from '../api_integration/services';
export const services = {
...commonServices,
legacyEs: apiIntegrationServices.legacyEs,
supertestWithoutAuth: apiIntegrationServices.supertestWithoutAuth,
};

View file

@ -1,14 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { FtrProviderContext } from '../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('apis PKI', function () {
this.tags('ciGroup6');
loadTestFile(require.resolve('./security'));
});
}

View file

@ -1,11 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { GenericFtrProviderContext } from '@kbn/test/types/ftr';
import { services } from './services';
export type FtrProviderContext = GenericFtrProviderContext<typeof services, {}>;

View file

@ -1,14 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { services as commonServices } from '../common/services';
import { services as apiIntegrationServices } from '../api_integration/services';
export const services = {
...commonServices,
esSupertest: apiIntegrationServices.esSupertest,
supertestWithoutAuth: apiIntegrationServices.supertestWithoutAuth,
};

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { PluginInitializer } from '../../../../../../src/core/server';
import type { PluginInitializer } from '../../../../../../../src/core/server';
import { initRoutes } from './init_routes';
export const plugin: PluginInitializer<void, void> = () => ({

View file

@ -5,7 +5,7 @@
*/
import { schema } from '@kbn/config-schema';
import { IRouter } from '../../../../../../src/core/server';
import type { IRouter } from '../../../../../../../src/core/server';
import { createTokens } from '../../oidc_tools';
export function initRoutes(router: IRouter) {

View file

@ -11,21 +11,15 @@ import { services } from './services';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts'));
const kerberosKeytabPath = resolve(
__dirname,
'../../test/kerberos_api_integration/fixtures/krb5.keytab'
);
const kerberosConfigPath = resolve(
__dirname,
'../../test/kerberos_api_integration/fixtures/krb5.conf'
);
const kerberosKeytabPath = resolve(__dirname, './fixtures/kerberos/krb5.keytab');
const kerberosConfigPath = resolve(__dirname, './fixtures/kerberos/krb5.conf');
return {
testFiles: [require.resolve('./apis')],
testFiles: [require.resolve('./tests/kerberos')],
servers: xPackAPITestsConfig.get('servers'),
services,
junit: {
reportName: 'X-Pack Kerberos API Integration Tests',
reportName: 'X-Pack Security API Integration Tests (Kerberos)',
},
esTestCluster: {

View file

@ -7,13 +7,13 @@
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const kerberosAPITestsConfig = await readConfigFile(require.resolve('./config.ts'));
const kerberosAPITestsConfig = await readConfigFile(require.resolve('./kerberos.config.ts'));
return {
...kerberosAPITestsConfig.getAll(),
junit: {
reportName: 'X-Pack Kerberos API with Anonymous Access Integration Tests',
reportName: 'X-Pack Security API Integration Tests (Kerberos with Anonymous Access)',
},
esTestCluster: {

View file

@ -15,13 +15,13 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts'));
const kibanaPort = xPackAPITestsConfig.get('servers.kibana.port');
const kerberosKeytabPath = resolve(__dirname, '../kerberos_api_integration/fixtures/krb5.keytab');
const kerberosConfigPath = resolve(__dirname, '../kerberos_api_integration/fixtures/krb5.conf');
const kerberosKeytabPath = resolve(__dirname, './fixtures/kerberos/krb5.keytab');
const kerberosConfigPath = resolve(__dirname, './fixtures/kerberos/krb5.conf');
const oidcJWKSPath = resolve(__dirname, '../oidc_api_integration/fixtures/jwks.json');
const oidcIdPPlugin = resolve(__dirname, '../oidc_api_integration/fixtures/oidc_provider');
const oidcJWKSPath = resolve(__dirname, './fixtures/oidc/jwks.json');
const oidcIdPPlugin = resolve(__dirname, './fixtures/oidc/oidc_provider');
const pkiKibanaCAPath = resolve(__dirname, '../pki_api_integration/fixtures/kibana_ca.crt');
const pkiKibanaCAPath = resolve(__dirname, './fixtures/pki/kibana_ca.crt');
const saml1IdPMetadataPath = resolve(__dirname, './fixtures/saml/idp_metadata.xml');
const saml2IdPMetadataPath = resolve(__dirname, './fixtures/saml/idp_metadata_2.xml');

View file

@ -10,17 +10,17 @@ import { services } from './services';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts'));
const plugin = resolve(__dirname, './fixtures/oidc_provider');
const plugin = resolve(__dirname, './fixtures/oidc/oidc_provider');
const kibanaPort = xPackAPITestsConfig.get('servers.kibana.port');
const jwksPath = resolve(__dirname, './fixtures/jwks.json');
const jwksPath = resolve(__dirname, './fixtures/oidc/jwks.json');
return {
testFiles: [require.resolve('./apis/authorization_code_flow')],
testFiles: [require.resolve('./tests/oidc/authorization_code_flow')],
servers: xPackAPITestsConfig.get('servers'),
security: { disableTestUser: true },
services,
junit: {
reportName: 'X-Pack OpenID Connect API Integration Tests',
reportName: 'X-Pack Security API Integration Tests (OIDC - Authorization Code Flow)',
},
esTestCluster: {

View file

@ -7,14 +7,14 @@
import { FtrConfigProviderContext } from '@kbn/test/types/ftr';
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
const oidcAPITestsConfig = await readConfigFile(require.resolve('./config.ts'));
const oidcAPITestsConfig = await readConfigFile(require.resolve('./oidc.config.ts'));
return {
...oidcAPITestsConfig.getAll(),
testFiles: [require.resolve('./apis/implicit_flow')],
testFiles: [require.resolve('./tests/oidc/implicit_flow')],
junit: {
reportName: 'X-Pack OpenID Connect API Integration Tests (Implicit Flow)',
reportName: 'X-Pack Security API Integration Tests (OIDC - Implicit Flow)',
},
esTestCluster: {

View file

@ -25,12 +25,12 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
};
return {
testFiles: [require.resolve('./apis')],
testFiles: [require.resolve('./tests/pki')],
servers,
security: { disableTestUser: true },
services,
junit: {
reportName: 'X-Pack PKI API Integration Tests',
reportName: 'X-Pack Security API Integration Tests (PKI)',
},
esTestCluster: {
@ -58,7 +58,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
`--server.ssl.certificate=${KBN_CERT_PATH}`,
`--server.ssl.certificateAuthorities=${JSON.stringify([
CA_CERT_PATH,
resolve(__dirname, './fixtures/kibana_ca.crt'),
resolve(__dirname, './fixtures/pki/kibana_ca.crt'),
])}`,
`--server.ssl.clientAuthentication=required`,
`--elasticsearch.hosts=${servers.elasticsearch.protocol}://${servers.elasticsearch.hostname}:${servers.elasticsearch.port}`,

View file

@ -9,5 +9,6 @@ import { services as apiIntegrationServices } from '../api_integration/services'
export const services = {
...commonServices,
esSupertest: apiIntegrationServices.esSupertest,
supertestWithoutAuth: apiIntegrationServices.supertestWithoutAuth,
};

View file

@ -7,7 +7,9 @@
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('security', () => {
describe('security APIs - Kerberos', function () {
this.tags('ciGroup6');
loadTestFile(require.resolve('./kerberos_login'));
});
}

View file

@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../ftr_provider_context';
import {
getMutualAuthenticationResponseToken,
getSPNEGOToken,
} from '../../fixtures/kerberos_tools';
} from '../../fixtures/kerberos/kerberos_tools';
export default function ({ getService }: FtrProviderContext) {
const spnegoToken = getSPNEGOToken();
@ -92,21 +92,21 @@ export default function ({ getService }: FtrProviderContext) {
expect(spnegoResponse.headers['www-authenticate']).to.be('Negotiate');
});
it('AJAX requests should properly initiate SPNEGO', async () => {
it('AJAX requests should not initiate SPNEGO', async () => {
const ajaxResponse = await supertest
.get('/abc/xyz/spnego?one=two three')
.set('kbn-xsrf', 'xxx')
.expect(401);
expect(ajaxResponse.headers['set-cookie']).to.be(undefined);
expect(ajaxResponse.headers['www-authenticate']).to.be('Negotiate');
expect(ajaxResponse.headers['www-authenticate']).to.be(undefined);
});
});
describe('finishing SPNEGO', () => {
it('should properly set cookie and authenticate user', async () => {
const response = await supertest
.get('/internal/security/me')
.get('/security/account')
.set('Authorization', `Negotiate ${spnegoToken}`)
.expect(200);
@ -153,7 +153,7 @@ export default function ({ getService }: FtrProviderContext) {
it('should re-initiate SPNEGO handshake if token is rejected with 401', async () => {
const spnegoResponse = await supertest
.get('/internal/security/me')
.get('/security/account')
.set('Authorization', `Negotiate ${Buffer.from('Hello').toString('base64')}`)
.expect(401);
expect(spnegoResponse.headers['set-cookie']).to.be(undefined);
@ -162,7 +162,7 @@ export default function ({ getService }: FtrProviderContext) {
it('should fail if SPNEGO token is rejected because of unknown reason', async () => {
const spnegoResponse = await supertest
.get('/internal/security/me')
.get('/security/account')
.set('Authorization', 'Negotiate (:I am malformed:)')
.expect(500);
expect(spnegoResponse.headers['set-cookie']).to.be(undefined);
@ -175,7 +175,7 @@ export default function ({ getService }: FtrProviderContext) {
beforeEach(async () => {
const response = await supertest
.get('/internal/security/me')
.get('/security/account')
.set('Authorization', `Negotiate ${spnegoToken}`)
.expect(200);
@ -239,7 +239,7 @@ export default function ({ getService }: FtrProviderContext) {
it('should redirect to `logged_out` page after successful logout', async () => {
// First authenticate user to retrieve session cookie.
const response = await supertest
.get('/internal/security/me')
.get('/security/account')
.set('Authorization', `Negotiate ${spnegoToken}`)
.expect(200);
@ -274,7 +274,9 @@ export default function ({ getService }: FtrProviderContext) {
expect(cookies).to.have.length(1);
checkCookieIsCleared(request.cookie(cookies[0])!);
expect(apiResponse.headers['www-authenticate']).to.be('Negotiate');
// Request with a session cookie that is linked to an invalidated/non-existent session is treated the same as
// request without any session cookie at all.
expect(apiResponse.headers['www-authenticate']).to.be(undefined);
});
it('should redirect to home page if session cookie is not provided', async () => {
@ -290,7 +292,7 @@ export default function ({ getService }: FtrProviderContext) {
beforeEach(async () => {
const response = await supertest
.get('/internal/security/me')
.get('/security/account')
.set('Authorization', `Negotiate ${spnegoToken}`)
.expect(200);
@ -342,7 +344,7 @@ export default function ({ getService }: FtrProviderContext) {
// 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')
.get('/security/account')
.set('Cookie', sessionCookie.cookieString())
.expect(200);
@ -368,7 +370,7 @@ export default function ({ getService }: FtrProviderContext) {
beforeEach(async () => {
const response = await supertest
.get('/internal/security/me')
.get('/security/account')
.set('Authorization', `Negotiate ${spnegoToken}`)
.expect(200);
@ -405,7 +407,7 @@ export default function ({ getService }: FtrProviderContext) {
it('non-AJAX call should initiate SPNEGO and clear existing cookie', async function () {
const nonAjaxResponse = await supertest
.get('/')
.get('/security/account')
.set('Cookie', sessionCookie.cookieString())
.expect(401);

View file

@ -11,11 +11,11 @@ import url from 'url';
import { CA_CERT_PATH } from '@kbn/dev-utils';
import expect from '@kbn/expect';
import type { AuthenticationProvider } from '../../../../plugins/security/common/types';
import { getStateAndNonce } from '../../../oidc_api_integration/fixtures/oidc_tools';
import { getStateAndNonce } from '../../fixtures/oidc/oidc_tools';
import {
getMutualAuthenticationResponseToken,
getSPNEGOToken,
} from '../../../kerberos_api_integration/fixtures/kerberos_tools';
} from '../../fixtures/kerberos/kerberos_tools';
import { getSAMLRequestId, getSAMLResponse } from '../../fixtures/saml/saml_tools';
import { FtrProviderContext } from '../../ftr_provider_context';
@ -29,9 +29,7 @@ export default function ({ getService }: FtrProviderContext) {
const validPassword = kibanaServerConfig.password;
const CA_CERT = readFileSync(CA_CERT_PATH);
const CLIENT_CERT = readFileSync(
resolve(__dirname, '../../../pki_api_integration/fixtures/first_client.p12')
);
const CLIENT_CERT = readFileSync(resolve(__dirname, '../../fixtures/pki/first_client.p12'));
async function checkSessionCookie(
sessionCookie: Cookie,

View file

@ -4,10 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { FtrProviderContext } from '../../ftr_provider_context';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('apis', function () {
describe('security APIs - OIDC (Authorization Code Flow)', function () {
this.tags('ciGroup6');
loadTestFile(require.resolve('./oidc_auth'));
});

View file

@ -8,8 +8,8 @@ import expect from '@kbn/expect';
import request, { Cookie } from 'request';
import url from 'url';
import { delay } from 'bluebird';
import { getStateAndNonce } from '../../fixtures/oidc_tools';
import { FtrProviderContext } from '../../ftr_provider_context';
import { getStateAndNonce } from '../../../fixtures/oidc/oidc_tools';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertestWithoutAuth');

View file

@ -4,10 +4,10 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { FtrProviderContext } from '../../ftr_provider_context';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('apis', function () {
describe('security APIs - OIDC (Implicit Flow)', function () {
this.tags('ciGroup6');
loadTestFile(require.resolve('./oidc_auth'));
});

View file

@ -8,8 +8,8 @@ import expect from '@kbn/expect';
import { JSDOM } from 'jsdom';
import request, { Cookie } from 'request';
import { format as formatURL } from 'url';
import { createTokens, getStateAndNonce } from '../../fixtures/oidc_tools';
import { FtrProviderContext } from '../../ftr_provider_context';
import { createTokens, getStateAndNonce } from '../../../fixtures/oidc/oidc_tools';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const supertest = getService('supertestWithoutAuth');

View file

@ -7,7 +7,9 @@
import { FtrProviderContext } from '../../ftr_provider_context';
export default function ({ loadTestFile }: FtrProviderContext) {
describe('security', () => {
describe('security APIs - PKI', function () {
this.tags('ciGroup6');
loadTestFile(require.resolve('./pki_auth'));
});
}

View file

@ -13,10 +13,10 @@ 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 FIRST_CLIENT_CERT = readFileSync(resolve(__dirname, '../../fixtures/pki/first_client.p12'));
const SECOND_CLIENT_CERT = readFileSync(resolve(__dirname, '../../fixtures/pki/second_client.p12'));
const UNTRUSTED_CLIENT_CERT = readFileSync(
resolve(__dirname, '../../fixtures/untrusted_client.p12')
resolve(__dirname, '../../fixtures/pki/untrusted_client.p12')
);
export default function ({ getService }: FtrProviderContext) {
@ -97,9 +97,20 @@ export default function ({ getService }: FtrProviderContext) {
// Do not assert on the `authentication_realm`, as the value differs for on-prem vs cloud
});
it('AJAX requests should not create a new session', async () => {
const ajaxResponse = await supertest
.get('/internal/security/me')
.set('kbn-xsrf', 'xxx')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.expect(401);
expect(ajaxResponse.headers['set-cookie']).to.be(undefined);
});
it('should properly set cookie and authenticate user', async () => {
const response = await supertest
.get('/internal/security/me')
.get('/security/account')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.expect(200);
@ -110,35 +121,34 @@ export default function ({ getService }: FtrProviderContext) {
const sessionCookie = request.cookie(cookies[0])!;
checkCookieIsSet(sessionCookie);
expect(response.body).to.eql({
username: 'first_client',
roles: ['kibana_admin'],
full_name: null,
email: null,
enabled: true,
metadata: {
pki_delegated_by_realm: 'reserved',
pki_delegated_by_user: 'kibana',
pki_dn: 'CN=first_client',
},
authentication_realm: { name: 'pki1', type: 'pki' },
lookup_realm: { name: 'pki1', type: 'pki' },
authentication_provider: { name: 'pki', type: 'pki' },
authentication_type: 'token',
});
// Cookie should be accepted.
await supertest
.get('/internal/security/me')
.set('kbn-xsrf', 'xxx')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.set('Cookie', sessionCookie.cookieString())
.expect(200);
.expect(200, {
username: 'first_client',
roles: ['kibana_admin'],
full_name: null,
email: null,
enabled: true,
metadata: {
pki_delegated_by_realm: 'reserved',
pki_delegated_by_user: 'kibana',
pki_dn: 'CN=first_client',
},
authentication_realm: { name: 'pki1', type: 'pki' },
lookup_realm: { name: 'pki1', type: 'pki' },
authentication_provider: { name: 'pki', type: 'pki' },
authentication_type: 'token',
});
});
it('should update session if new certificate is provided', async () => {
let response = await supertest
.get('/internal/security/me')
.get('/security/account')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.expect(200);
@ -177,7 +187,7 @@ export default function ({ getService }: FtrProviderContext) {
it('should reject valid cookie if used with untrusted certificate', async () => {
const response = await supertest
.get('/internal/security/me')
.get('/security/account')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.expect(200);
@ -201,7 +211,7 @@ export default function ({ getService }: FtrProviderContext) {
beforeEach(async () => {
const response = await supertest
.get('/internal/security/me')
.get('/security/account')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.expect(200);
@ -274,7 +284,7 @@ export default function ({ getService }: FtrProviderContext) {
it('should redirect to `logged_out` page after successful logout', async () => {
// First authenticate user to retrieve session cookie.
const response = await supertest
.get('/internal/security/me')
.get('/security/account')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.expect(200);
@ -317,7 +327,7 @@ export default function ({ getService }: FtrProviderContext) {
beforeEach(async () => {
const response = await supertest
.get('/internal/security/me')
.get('/security/account')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.expect(200);
@ -363,7 +373,7 @@ export default function ({ getService }: FtrProviderContext) {
// 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')
.get('/security/account')
.ca(CA_CERT)
.pfx(FIRST_CLIENT_CERT)
.set('Cookie', sessionCookie.cookieString())

View file

@ -20,8 +20,11 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
);
const kibanaPort = kibanaFunctionalConfig.get('servers.kibana.port');
const jwksPath = resolve(__dirname, '../oidc_api_integration/fixtures/jwks.json');
const oidcOpPPlugin = resolve(__dirname, '../oidc_api_integration/fixtures/oidc_provider');
const jwksPath = resolve(__dirname, '../security_api_integration/fixtures/oidc/jwks.json');
const oidcOpPPlugin = resolve(
__dirname,
'../security_api_integration/fixtures/oidc/oidc_provider'
);
return {
testFiles: [resolve(__dirname, './tests/oidc')],