mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
Allow Kibana to restrict the usage of JWT for a predefined set of routes only. (#163806)
## Summary Allow Kibana to restrict the usage of JWT for a predefined set of routes only in Serverless environment by default. This capability is not available in non-Serverless environment. Any route that needs to be accessed in Serverless environemnt using JWT as a means of authentication should include `security:acceptJWT` tag. ## How to test If you'd like to generate your own JWT to test the PR, please follow the steps outlined in https://github.com/elastic/kibana/pull/159117#issue-1743796706 or just run functional test server and use static JWT from the Serverless test. This PR also generated a Serverless Docker image that you can use in your Dev/QA MKI cluster. - [x] Implementation functionality and add unit tests - [x] Update metrics/status routes to include new `security:acceptJWT` tag - [x] Update serverless test suite to include a test for `security:acceptJWT` __Fixes: https://github.com/elastic/kibana/issues/162632__ --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
40ba6b619d
commit
5aee5da843
18 changed files with 363 additions and 10 deletions
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
@ -595,7 +595,7 @@ packages/kbn-search-api-panels @elastic/enterprise-search-frontend
|
|||
examples/search_examples @elastic/kibana-data-discovery
|
||||
packages/kbn-search-response-warnings @elastic/kibana-data-discovery
|
||||
x-pack/plugins/searchprofiler @elastic/platform-deployment-management
|
||||
x-pack/test/security_api_integration/packages/helpers @elastic/kibana-core
|
||||
x-pack/test/security_api_integration/packages/helpers @elastic/kibana-security
|
||||
x-pack/plugins/security @elastic/kibana-security
|
||||
x-pack/plugins/security_solution_ess @elastic/security-solution
|
||||
x-pack/test/cases_api_integration/common/plugins/security_solution @elastic/response-ops
|
||||
|
|
|
@ -82,7 +82,10 @@ export const registerStatusRoute = ({
|
|||
path: '/api/status',
|
||||
options: {
|
||||
authRequired: 'optional',
|
||||
tags: ['api'], // ensures that unauthenticated calls receive a 401 rather than a 302 redirect to login page
|
||||
// The `api` tag ensures that unauthenticated calls receive a 401 rather than a 302 redirect to login page.
|
||||
// The `security:acceptJWT` tag allows route to be accessed with JWT credentials. It points to
|
||||
// ROUTE_TAG_ACCEPT_JWT from '@kbn/security-plugin/server' that cannot be imported here directly.
|
||||
tags: ['api', 'security:acceptJWT'],
|
||||
access: 'public', // needs to be public to allow access from "system" users like k8s readiness probes.
|
||||
},
|
||||
validate: {
|
||||
|
|
|
@ -55,7 +55,10 @@ export function registerStatsRoute({
|
|||
path: '/api/stats',
|
||||
options: {
|
||||
authRequired: !config.allowAnonymous,
|
||||
tags: ['api'], // ensures that unauthenticated calls receive a 401 rather than a 302 redirect to login page
|
||||
// The `api` tag ensures that unauthenticated calls receive a 401 rather than a 302 redirect to login page.
|
||||
// The `security:acceptJWT` tag allows route to be accessed with JWT credentials. It points to
|
||||
// ROUTE_TAG_ACCEPT_JWT from '@kbn/security-plugin/server' that cannot be imported here directly.
|
||||
tags: ['api', 'security:acceptJWT'],
|
||||
access: 'public', // needs to be public to allow access from "system" users like metricbeat.
|
||||
},
|
||||
validate: {
|
||||
|
|
|
@ -62,12 +62,14 @@ function getMockOptions({
|
|||
selector,
|
||||
accessAgreementMessage,
|
||||
customLogoutURL,
|
||||
configContext = {},
|
||||
}: {
|
||||
providers?: Record<string, unknown> | string[];
|
||||
http?: Partial<AuthenticatorOptions['config']['authc']['http']>;
|
||||
selector?: AuthenticatorOptions['config']['authc']['selector'];
|
||||
accessAgreementMessage?: string;
|
||||
customLogoutURL?: string;
|
||||
configContext?: Record<string, unknown>;
|
||||
} = {}) {
|
||||
const auditService = auditServiceMock.create();
|
||||
auditLogger = auditLoggerMock.create();
|
||||
|
@ -86,10 +88,10 @@ function getMockOptions({
|
|||
loggers: loggingSystemMock.create(),
|
||||
getServerBaseURL: jest.fn(),
|
||||
config: createConfig(
|
||||
ConfigSchema.validate({
|
||||
authc: { selector, providers, http },
|
||||
...accessAgreementObj,
|
||||
}),
|
||||
ConfigSchema.validate(
|
||||
{ authc: { selector, providers, http }, ...accessAgreementObj },
|
||||
configContext
|
||||
),
|
||||
loggingSystemMock.create().get(),
|
||||
{ isTLSEnabled: false }
|
||||
),
|
||||
|
@ -317,6 +319,23 @@ describe('Authenticator', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('includes JWT options if specified', () => {
|
||||
new Authenticator(
|
||||
getMockOptions({
|
||||
providers: { basic: { basic1: { order: 0 } } },
|
||||
http: { jwt: { taggedRoutesOnly: true } },
|
||||
configContext: { serverless: true },
|
||||
})
|
||||
);
|
||||
|
||||
expect(
|
||||
jest.requireMock('./providers/http').HTTPAuthenticationProvider
|
||||
).toHaveBeenCalledWith(expect.anything(), {
|
||||
supportedSchemes: new Set(['apikey', 'bearer', 'basic']),
|
||||
jwt: { taggedRoutesOnly: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('does not include additional schemes if `autoSchemesEnabled` is disabled', () => {
|
||||
new Authenticator(
|
||||
getMockOptions({
|
||||
|
|
|
@ -648,7 +648,13 @@ export class Authenticator {
|
|||
throw new Error(`Provider name "${options.name}" is reserved.`);
|
||||
}
|
||||
|
||||
this.providers.set(options.name, new HTTPAuthenticationProvider(options, { supportedSchemes }));
|
||||
this.providers.set(
|
||||
options.name,
|
||||
new HTTPAuthenticationProvider(options, {
|
||||
supportedSchemes,
|
||||
jwt: this.options.config.authc.http.jwt,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -15,6 +15,7 @@ import { mockAuthenticationProviderOptions } from './base.mock';
|
|||
import { HTTPAuthenticationProvider } from './http';
|
||||
import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock';
|
||||
import { securityMock } from '../../mocks';
|
||||
import { ROUTE_TAG_ACCEPT_JWT } from '../../routes/tags';
|
||||
import { AuthenticationResult } from '../authentication_result';
|
||||
import { DeauthenticationResult } from '../deauthentication_result';
|
||||
|
||||
|
@ -144,6 +145,113 @@ describe('HTTPAuthenticationProvider', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('succeeds for JWT authentication if not restricted to tagged routes.', async () => {
|
||||
const header = 'Bearer header.body.signature';
|
||||
const user = mockAuthenticatedUser({ authentication_realm: { name: 'jwt1', type: 'jwt' } });
|
||||
const request = httpServerMock.createKibanaRequest({ headers: { authorization: header } });
|
||||
|
||||
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
|
||||
mockScopedClusterClient.asCurrentUser.security.authenticate.mockResponse(user);
|
||||
mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient);
|
||||
mockOptions.client.asScoped.mockClear();
|
||||
|
||||
const provider = new HTTPAuthenticationProvider(mockOptions, {
|
||||
supportedSchemes: new Set(['bearer']),
|
||||
});
|
||||
|
||||
await expect(provider.authenticate(request)).resolves.toEqual(
|
||||
AuthenticationResult.succeeded({
|
||||
...user,
|
||||
authentication_provider: { type: 'http', name: 'http' },
|
||||
})
|
||||
);
|
||||
|
||||
expectAuthenticateCall(mockOptions.client, { headers: { authorization: header } });
|
||||
|
||||
expect(request.headers.authorization).toBe(header);
|
||||
});
|
||||
|
||||
it('succeeds for non-JWT authentication if JWT restricted to tagged routes.', async () => {
|
||||
const header = 'Basic xxx';
|
||||
const user = mockAuthenticatedUser();
|
||||
const request = httpServerMock.createKibanaRequest({ headers: { authorization: header } });
|
||||
|
||||
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
|
||||
mockScopedClusterClient.asCurrentUser.security.authenticate.mockResponse(user);
|
||||
mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient);
|
||||
mockOptions.client.asScoped.mockClear();
|
||||
|
||||
const provider = new HTTPAuthenticationProvider(mockOptions, {
|
||||
supportedSchemes: new Set(['bearer', 'basic']),
|
||||
jwt: { taggedRoutesOnly: true },
|
||||
});
|
||||
|
||||
await expect(provider.authenticate(request)).resolves.toEqual(
|
||||
AuthenticationResult.succeeded({
|
||||
...user,
|
||||
authentication_provider: { type: 'http', name: 'http' },
|
||||
})
|
||||
);
|
||||
|
||||
expectAuthenticateCall(mockOptions.client, { headers: { authorization: header } });
|
||||
|
||||
expect(request.headers.authorization).toBe(header);
|
||||
});
|
||||
|
||||
it('succeeds for JWT authentication if restricted to tagged routes and route is tagged.', async () => {
|
||||
const header = 'Bearer header.body.signature';
|
||||
const user = mockAuthenticatedUser({ authentication_realm: { name: 'jwt1', type: 'jwt' } });
|
||||
const request = httpServerMock.createKibanaRequest({
|
||||
headers: { authorization: header },
|
||||
routeTags: [ROUTE_TAG_ACCEPT_JWT],
|
||||
});
|
||||
|
||||
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
|
||||
mockScopedClusterClient.asCurrentUser.security.authenticate.mockResponse(user);
|
||||
mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient);
|
||||
mockOptions.client.asScoped.mockClear();
|
||||
|
||||
const provider = new HTTPAuthenticationProvider(mockOptions, {
|
||||
supportedSchemes: new Set(['bearer']),
|
||||
jwt: { taggedRoutesOnly: true },
|
||||
});
|
||||
|
||||
await expect(provider.authenticate(request)).resolves.toEqual(
|
||||
AuthenticationResult.succeeded({
|
||||
...user,
|
||||
authentication_provider: { type: 'http', name: 'http' },
|
||||
})
|
||||
);
|
||||
|
||||
expectAuthenticateCall(mockOptions.client, { headers: { authorization: header } });
|
||||
|
||||
expect(request.headers.authorization).toBe(header);
|
||||
});
|
||||
|
||||
it('fails for JWT authentication if restricted to tagged routes and route is NOT tagged.', async () => {
|
||||
const header = 'Bearer header.body.signature';
|
||||
const user = mockAuthenticatedUser({ authentication_realm: { name: 'jwt1', type: 'jwt' } });
|
||||
const request = httpServerMock.createKibanaRequest({ headers: { authorization: header } });
|
||||
|
||||
const mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
|
||||
mockScopedClusterClient.asCurrentUser.security.authenticate.mockResponse(user);
|
||||
mockOptions.client.asScoped.mockReturnValue(mockScopedClusterClient);
|
||||
mockOptions.client.asScoped.mockClear();
|
||||
|
||||
const provider = new HTTPAuthenticationProvider(mockOptions, {
|
||||
supportedSchemes: new Set(['bearer']),
|
||||
jwt: { taggedRoutesOnly: true },
|
||||
});
|
||||
|
||||
await expect(provider.authenticate(request)).resolves.toEqual(
|
||||
AuthenticationResult.notHandled()
|
||||
);
|
||||
|
||||
expectAuthenticateCall(mockOptions.client, { headers: { authorization: header } });
|
||||
|
||||
expect(request.headers.authorization).toBe(header);
|
||||
});
|
||||
|
||||
it('fails if authentication via `authorization` header with supported scheme fails.', async () => {
|
||||
const failureReason = new errors.ResponseError(securityMock.createApiResponse({ body: {} }));
|
||||
for (const { schemes, header } of [
|
||||
|
|
|
@ -9,12 +9,22 @@ import type { KibanaRequest } from '@kbn/core/server';
|
|||
|
||||
import type { AuthenticationProviderOptions } from './base';
|
||||
import { BaseAuthenticationProvider } from './base';
|
||||
import { ROUTE_TAG_ACCEPT_JWT } from '../../routes/tags';
|
||||
import { AuthenticationResult } from '../authentication_result';
|
||||
import { DeauthenticationResult } from '../deauthentication_result';
|
||||
import { HTTPAuthorizationHeader } from '../http_authentication';
|
||||
|
||||
/**
|
||||
* A type-string of the Elasticsearch JWT realm.
|
||||
*/
|
||||
const JWT_REALM_TYPE = 'jwt';
|
||||
|
||||
interface HTTPAuthenticationProviderOptions {
|
||||
supportedSchemes: Set<string>;
|
||||
jwt?: {
|
||||
// When set, only routes marked with `ROUTE_TAG_ACCEPT_JWT` tag will accept JWT as a means of authentication.
|
||||
taggedRoutesOnly: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -32,6 +42,11 @@ export class HTTPAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
*/
|
||||
private readonly supportedSchemes: Set<string>;
|
||||
|
||||
/**
|
||||
* Options relevant to the JWT authentication.
|
||||
*/
|
||||
private readonly jwt: HTTPAuthenticationProviderOptions['jwt'];
|
||||
|
||||
constructor(
|
||||
protected readonly options: Readonly<AuthenticationProviderOptions>,
|
||||
httpOptions: Readonly<HTTPAuthenticationProviderOptions>
|
||||
|
@ -44,6 +59,7 @@ export class HTTPAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
this.supportedSchemes = new Set(
|
||||
[...httpOptions.supportedSchemes].map((scheme) => scheme.toLowerCase())
|
||||
);
|
||||
this.jwt = httpOptions.jwt;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -79,6 +95,23 @@ export class HTTPAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
this.logger.debug(
|
||||
`Request to ${request.url.pathname}${request.url.search} has been authenticated via authorization header with "${authorizationHeader.scheme}" scheme.`
|
||||
);
|
||||
|
||||
// If Kibana is configured to restrict JWT authentication only to selected routes, ensure that the route is marked
|
||||
// with the `ROUTE_TAG_ACCEPT_JWT` tag to bypass that restriction.
|
||||
if (
|
||||
user.authentication_realm.type === JWT_REALM_TYPE &&
|
||||
this.jwt?.taggedRoutesOnly &&
|
||||
!request.route.options.tags.includes(ROUTE_TAG_ACCEPT_JWT)
|
||||
) {
|
||||
// Log a portion of the JWT signature to make debugging easier.
|
||||
const jwtExcerpt = authorizationHeader.credentials.slice(-10);
|
||||
this.logger.error(
|
||||
`Attempted to authenticate with JWT credentials (…${jwtExcerpt}) against ${request.url.pathname}${request.url.search}, but it's not allowed. ` +
|
||||
`Ensure that the route is defined with the "${ROUTE_TAG_ACCEPT_JWT}" tag.`
|
||||
);
|
||||
return AuthenticationResult.notHandled();
|
||||
}
|
||||
|
||||
return AuthenticationResult.succeeded(user);
|
||||
} catch (err) {
|
||||
this.logger.debug(
|
||||
|
|
|
@ -183,6 +183,68 @@ describe('config schema', () => {
|
|||
"showNavLinks": true,
|
||||
}
|
||||
`);
|
||||
|
||||
expect(ConfigSchema.validate({}, { serverless: true, dist: true })).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"audit": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"authc": Object {
|
||||
"http": Object {
|
||||
"autoSchemesEnabled": true,
|
||||
"enabled": true,
|
||||
"jwt": Object {
|
||||
"taggedRoutesOnly": true,
|
||||
},
|
||||
"schemes": Array [
|
||||
"apikey",
|
||||
"bearer",
|
||||
],
|
||||
},
|
||||
"providers": Object {
|
||||
"anonymous": undefined,
|
||||
"basic": Object {
|
||||
"basic": Object {
|
||||
"accessAgreement": undefined,
|
||||
"description": undefined,
|
||||
"enabled": true,
|
||||
"hint": undefined,
|
||||
"icon": undefined,
|
||||
"order": 0,
|
||||
"session": Object {
|
||||
"idleTimeout": undefined,
|
||||
"lifespan": undefined,
|
||||
},
|
||||
"showInSelector": true,
|
||||
},
|
||||
},
|
||||
"kerberos": undefined,
|
||||
"oidc": undefined,
|
||||
"pki": undefined,
|
||||
"saml": undefined,
|
||||
"token": undefined,
|
||||
},
|
||||
"selector": Object {},
|
||||
},
|
||||
"cookieName": "sid",
|
||||
"enabled": true,
|
||||
"loginAssistanceMessage": "",
|
||||
"public": Object {},
|
||||
"secureCookies": false,
|
||||
"session": Object {
|
||||
"cleanupInterval": "PT1H",
|
||||
"idleTimeout": "P3D",
|
||||
"lifespan": "P30D",
|
||||
},
|
||||
"showInsecureClusterWarning": true,
|
||||
"showNavLinks": true,
|
||||
"ui": Object {
|
||||
"roleManagementEnabled": true,
|
||||
"roleMappingManagementEnabled": true,
|
||||
"userManagementEnabled": true,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should throw error if xpack.security.encryptionKey is less than 32 characters', () => {
|
||||
|
@ -1412,6 +1474,34 @@ describe('config schema', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('authc.http', () => {
|
||||
it('should not allow xpack.security.authc.http.jwt.* to be configured outside of the serverless context', () => {
|
||||
expect(() =>
|
||||
ConfigSchema.validate(
|
||||
{ authc: { http: { jwt: { taggedRoutesOnly: false } } } },
|
||||
{ serverless: false }
|
||||
)
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"[authc.http.jwt]: a value wasn't expected to be present"`
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow xpack.security.authc.http.jwt.* to be configured inside of the serverless context', () => {
|
||||
expect(
|
||||
ConfigSchema.validate(
|
||||
{ authc: { http: { jwt: { taggedRoutesOnly: false } } } },
|
||||
{ serverless: true }
|
||||
).ui
|
||||
).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"roleManagementEnabled": true,
|
||||
"roleMappingManagementEnabled": true,
|
||||
"userManagementEnabled": true,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ui', () => {
|
||||
it('should not allow xpack.security.ui.* to be configured outside of the serverless context', () => {
|
||||
expect(() =>
|
||||
|
|
|
@ -279,6 +279,11 @@ export const ConfigSchema = schema.object({
|
|||
enabled: schema.boolean({ defaultValue: true }),
|
||||
autoSchemesEnabled: schema.boolean({ defaultValue: true }),
|
||||
schemes: schema.arrayOf(schema.string(), { defaultValue: ['apikey', 'bearer'] }),
|
||||
jwt: offeringBasedSchema({
|
||||
serverless: schema.object({
|
||||
taggedRoutesOnly: schema.boolean({ defaultValue: true }),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
audit: schema.object({
|
||||
|
|
|
@ -25,3 +25,9 @@ export const ROUTE_TAG_CAN_REDIRECT = 'security:canRedirect';
|
|||
* parties, require special handling.
|
||||
*/
|
||||
export const ROUTE_TAG_AUTH_FLOW = 'security:authFlow';
|
||||
|
||||
/**
|
||||
* If `xpack.security.authc.http.jwt.taggedRoutesOnly` flag is set, then only routes marked with this tag will accept
|
||||
* JWT as a means of authentication.
|
||||
*/
|
||||
export const ROUTE_TAG_ACCEPT_JWT = 'security:acceptJWT';
|
||||
|
|
|
@ -57,11 +57,13 @@ describe('backgroundTaskUtilizationRoute', () => {
|
|||
`"/internal/task_manager/_background_task_utilization"`
|
||||
);
|
||||
expect(config1.options?.authRequired).toEqual(true);
|
||||
expect(config1.options?.tags).toEqual(['security:acceptJWT']);
|
||||
|
||||
const [config2] = router.get.mock.calls[1];
|
||||
|
||||
expect(config2.path).toMatchInlineSnapshot(`"/api/task_manager/_background_task_utilization"`);
|
||||
expect(config2.options?.authRequired).toEqual(true);
|
||||
expect(config2.options?.tags).toEqual(['security:acceptJWT']);
|
||||
});
|
||||
|
||||
it(`sets "authRequired" to false when config.unsafe.authenticate_background_task_utilization is set to false`, async () => {
|
||||
|
|
|
@ -117,6 +117,9 @@ export function backgroundTaskUtilizationRoute(
|
|||
options: {
|
||||
access: 'public', // access must be public to allow "system" users, like metrics collectors, to access these routes
|
||||
authRequired: routeOption.isAuthenticated ?? true,
|
||||
// The `security:acceptJWT` tag allows route to be accessed with JWT credentials. It points to
|
||||
// ROUTE_TAG_ACCEPT_JWT from '@kbn/security-plugin/server' that cannot be imported here directly.
|
||||
tags: ['security:acceptJWT'],
|
||||
},
|
||||
},
|
||||
async function (
|
||||
|
|
|
@ -28,6 +28,7 @@ describe('metricsRoute', () => {
|
|||
const [config] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/api/task_manager/metrics"`);
|
||||
expect(config.options?.tags).toEqual(['security:acceptJWT']);
|
||||
});
|
||||
|
||||
it('emits resetMetric$ event when route is accessed and reset query param is true', async () => {
|
||||
|
|
|
@ -48,6 +48,9 @@ export function metricsRoute(params: MetricsRouteParams) {
|
|||
path: `/api/task_manager/metrics`,
|
||||
options: {
|
||||
access: 'public',
|
||||
// The `security:acceptJWT` tag allows route to be accessed with JWT credentials. It points to
|
||||
// ROUTE_TAG_ACCEPT_JWT from '@kbn/security-plugin/server' that cannot be imported here directly.
|
||||
tags: ['security:acceptJWT'],
|
||||
},
|
||||
// Uncomment when we determine that we can restrict API usage to Global admins based on telemetry
|
||||
// options: { tags: ['access:taskManager'] },
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/security-api-integration-helpers",
|
||||
"owner": "@elastic/kibana-core",
|
||||
"owner": "@elastic/kibana-security",
|
||||
"devOnly": true
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./security/anonymous'));
|
||||
loadTestFile(require.resolve('./security/api_keys'));
|
||||
loadTestFile(require.resolve('./security/authentication'));
|
||||
loadTestFile(require.resolve('./security/authentication_http'));
|
||||
loadTestFile(require.resolve('./security/authorization'));
|
||||
loadTestFile(require.resolve('./security/misc'));
|
||||
loadTestFile(require.resolve('./security/response_headers'));
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const svlCommonApi = getService('svlCommonApi');
|
||||
const supertest = getService('supertestWithoutAuth');
|
||||
|
||||
describe('security/authentication/http', function () {
|
||||
it('allows JWT HTTP authentication only for selected routes', async () => {
|
||||
const jsonWebToken =
|
||||
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2tpYmFuYS5lbGFzdGljLmNvL2p3dC8iLCJzdWIiOiJlbGFzdGljLWFnZW50IiwiYXVkIjoiZWxhc3RpY3NlYXJjaCIsIm5hbWUiOiJFbGFzdGljIEFnZW50IiwiaWF0Ijo5NDY2ODQ4MDAsImV4cCI6NDA3MDkwODgwMH0.P7RHKZlLskS5DfVRqoVO4ivoIq9rXl2-GW6hhC9NvTSkwphYivcjpTVcyENZvxTTvJJNqcyx6rF3T-7otTTIHBOZIMhZauc5dob-sqcN_mT2htqm3BpSdlJlz60TBq6diOtlNhV212gQCEJMPZj0MNj7kZRj_GsECrTaU7FU0A3HAzkbdx15vQJMKZiFbbQCVI7-X2J0bZzQKIWfMHD-VgHFwOe6nomT-jbYIXtCBDd6fNj1zTKRl-_uzjVqNK-h8YW1h6tE4xvZmXyHQ1-9yNKZIWC7iEaPkBLaBKQulLU5MvW3AtVDUhzm6--5H1J85JH5QhRrnKYRon7ZW5q1AQ';
|
||||
|
||||
// Check 5 routes that are currently known to accept JWT as a means of authentication.
|
||||
for (const allowedPath of [
|
||||
'/api/status',
|
||||
'/api/stats',
|
||||
'/api/task_manager/_background_task_utilization',
|
||||
'/internal/task_manager/_background_task_utilization',
|
||||
'/api/task_manager/metrics',
|
||||
]) {
|
||||
await supertest
|
||||
.get(allowedPath)
|
||||
.set('Authorization', `Bearer ${jsonWebToken}`)
|
||||
.set('ES-Client-Authentication', 'SharedSecret my_super_secret')
|
||||
.set(svlCommonApi.getInternalRequestHeader())
|
||||
.expect(200);
|
||||
}
|
||||
|
||||
// Make sure it's not possible to use JWT to have interactive sessions.
|
||||
await supertest
|
||||
.get('/')
|
||||
.set('Authorization', `Bearer ${jsonWebToken}`)
|
||||
.set('ES-Client-Authentication', 'SharedSecret my_super_secret')
|
||||
.expect(401);
|
||||
|
||||
// Make sure it's not possible to use JWT to access any other APIs.
|
||||
await supertest
|
||||
.get('/internal/security/me')
|
||||
.set('Authorization', `Bearer ${jsonWebToken}`)
|
||||
.set('ES-Client-Authentication', 'SharedSecret my_super_secret')
|
||||
.set(svlCommonApi.getInternalRequestHeader())
|
||||
.expect(401);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -28,6 +28,8 @@ export default async () => {
|
|||
'../../test/security_api_integration/plugins/saml_provider'
|
||||
);
|
||||
|
||||
const jwksPath = require.resolve('@kbn/security-api-integration-helpers/oidc/jwks.json');
|
||||
|
||||
return {
|
||||
servers,
|
||||
|
||||
|
@ -35,8 +37,25 @@ export default async () => {
|
|||
license: 'trial',
|
||||
from: 'snapshot',
|
||||
serverArgs: [
|
||||
'xpack.security.authc.realms.file.file1.order=-100',
|
||||
|
||||
'xpack.security.authc.realms.jwt.jwt1.order=-98',
|
||||
`xpack.security.authc.realms.jwt.jwt1.token_type=access_token`,
|
||||
'xpack.security.authc.realms.jwt.jwt1.client_authentication.type=shared_secret',
|
||||
`xpack.security.authc.realms.jwt.jwt1.client_authentication.shared_secret=my_super_secret`,
|
||||
`xpack.security.authc.realms.jwt.jwt1.allowed_issuer=https://kibana.elastic.co/jwt/`,
|
||||
`xpack.security.authc.realms.jwt.jwt1.allowed_subjects=elastic-agent`,
|
||||
'xpack.security.authc.realms.jwt.jwt1.allowed_audiences=elasticsearch',
|
||||
`xpack.security.authc.realms.jwt.jwt1.allowed_signature_algorithms=[RS256]`,
|
||||
`xpack.security.authc.realms.jwt.jwt1.claims.principal=sub`,
|
||||
`xpack.security.authc.realms.jwt.jwt1.pkc_jwkset_path=${jwksPath}`,
|
||||
|
||||
// TODO: We should set this flag to `false` as soon as we fully migrate tests to SAML and file realms.
|
||||
`xpack.security.authc.realms.native.native1.enabled=true`,
|
||||
`xpack.security.authc.realms.native.native1.order=-97`,
|
||||
|
||||
'xpack.security.authc.token.enabled=true',
|
||||
'xpack.security.authc.realms.saml.cloud-saml-kibana.order=0',
|
||||
'xpack.security.authc.realms.saml.cloud-saml-kibana.order=101',
|
||||
`xpack.security.authc.realms.saml.cloud-saml-kibana.idp.metadata.path=${idpPath}`,
|
||||
'xpack.security.authc.realms.saml.cloud-saml-kibana.idp.entity_id=http://www.elastic.co/saml1',
|
||||
`xpack.security.authc.realms.saml.cloud-saml-kibana.sp.entity_id=http://localhost:${servers.kibana.port}`,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue