diff --git a/public/apps/login/login-page.tsx b/public/apps/login/login-page.tsx
index 22421e3a7..449bd39de 100644
--- a/public/apps/login/login-page.tsx
+++ b/public/apps/login/login-page.tsx
@@ -219,10 +219,11 @@ export function LoginPage(props: LoginPageDeps) {
renderLoginButton(AuthType.ANONYMOUS, ANONYMOUS_AUTH_LOGIN, anonymousConfig)
);
}
-
- formBody.push();
- formBody.push();
- formBody.push();
+ if (!authOpts.includes(AuthType.PROXY) || authOpts.length !== 2) {
+ formBody.push();
+ formBody.push();
+ formBody.push();
+ }
}
break;
}
@@ -238,6 +239,12 @@ export function LoginPage(props: LoginPageDeps) {
formBodyOp.push(renderLoginButton(AuthType.SAML, samlAuthLoginUrl, samlConfig));
break;
}
+ case AuthType.PROXY: {
+ // formBody.pop();
+ // formBody.pop();
+ // formBody.pop();
+ break;
+ }
default: {
setloginFailed(true);
setloginError(
diff --git a/server/auth/types/multiple/multi_auth.ts b/server/auth/types/multiple/multi_auth.ts
index 8763eaa4f..8c536a47a 100644
--- a/server/auth/types/multiple/multi_auth.ts
+++ b/server/auth/types/multiple/multi_auth.ts
@@ -29,7 +29,7 @@ import { ANONYMOUS_AUTH_LOGIN, AuthType, LOGIN_PAGE_URI } from '../../../../comm
import { composeNextUrlQueryParam } from '../../../utils/next_url';
import { MultiAuthRoutes } from './routes';
import { SecuritySessionCookie } from '../../../session/security_cookie';
-import { BasicAuthentication, OpenIdAuthentication, SamlAuthentication } from '../../types';
+import { BasicAuthentication, OpenIdAuthentication, ProxyAuthentication, SamlAuthentication } from '../../types';
export class MultipleAuthentication extends AuthenticationType {
private authTypes: string | string[];
@@ -93,6 +93,19 @@ export class MultipleAuthentication extends AuthenticationType {
this.authHandlers.set(AuthType.SAML, SamlAuth);
break;
}
+ case AuthType.PROXY: {
+ const ProxyAuth = new ProxyAuthentication(
+ this.config,
+ this.sessionStorageFactory,
+ this.router,
+ this.esClient,
+ this.coreSetup,
+ this.logger
+ );
+ await ProxyAuth.init();
+ this.authHandlers.set(AuthType.PROXY, ProxyAuth);
+ break;
+ }
default: {
throw new Error(`Unsupported authentication type: ${this.authTypes[i]}`);
}
@@ -115,7 +128,7 @@ export class MultipleAuthentication extends AuthenticationType {
async getAdditionalAuthHeader(
request: OpenSearchDashboardsRequest
): Promise {
- // To Do: refactor this method to improve the effiency to get cookie, get cookie from input parameter
+ // To Do: refactor this method to improve the efficiency to get cookie, get cookie from input parameter
const cookie = await this.sessionStorageFactory.asScoped(request).get();
const reqAuthType = cookie?.authType?.toLowerCase();
diff --git a/server/auth/types/proxy/proxy_auth.test.ts b/server/auth/types/proxy/proxy_auth.test.ts
new file mode 100644
index 000000000..95e319056
--- /dev/null
+++ b/server/auth/types/proxy/proxy_auth.test.ts
@@ -0,0 +1,93 @@
+import { httpServerMock } from '../../../../../../src/core/server/http/http_server.mocks';
+import { IRouter, OpenSearchDashboardsRequest } from '../../../../../../src/core/server/http/router';
+import { SecurityPluginConfigType } from '../../../index';
+import { SecuritySessionCookie } from '../../../session/security_cookie';
+import { deflateValue } from '../../../utils/compression';
+import {
+ CoreSetup,
+ ILegacyClusterClient,
+ Logger,
+ SessionStorageFactory,
+} from '../../../../../../src/core/server';
+import { ProxyAuthentication } from './proxy_auth';
+
+describe('ProxyAuthentication', () => {
+ let proxyAuthentication: ProxyAuthentication;
+ let router: IRouter;
+ let core: CoreSetup;
+ let esClient: ILegacyClusterClient;
+ let sessionStorageFactory: SessionStorageFactory;
+ let logger: Logger;
+
+ beforeEach(() => {
+ // Set up mock dependencies
+ router = {} as IRouter;
+ core = {} as CoreSetup;
+ esClient = {} as ILegacyClusterClient;
+ sessionStorageFactory = {} as SessionStorageFactory;
+ logger = {} as Logger;
+
+ const config = ({
+ proxy: {
+ extra_storage: {
+ cookie_prefix: 'testcookie',
+ additional_cookies: 5,
+ },
+ },
+ } as unknown) as SecurityPluginConfigType;
+
+ proxyAuthentication = new ProxyAuthentication(
+ config,
+ sessionStorageFactory,
+ router,
+ esClient,
+ core,
+ logger
+ );
+ });
+
+ it('should build auth header from cookie with authHeaderValue', () => {
+
+ const cookie: SecuritySessionCookie = {
+ credentials: {
+ authHeaderValue: 'Bearer eyToken',
+ },
+ };
+
+ const expectedHeaders = {
+ authorization: 'Bearer eyToken',
+ };
+
+ const headers = proxyAuthentication.buildAuthHeaderFromCookie(cookie);
+
+ expect(headers).toEqual(expectedHeaders);
+ });
+
+ it('should get authHeaderValue from split cookies', () => {
+ const testString = 'Bearer eyCombinedToken';
+ const testStringBuffer: Buffer = deflateValue(testString);
+ const cookieValue = testStringBuffer.toString('base64');
+ const cookiePrefix = 'testcookie';
+ const splitValueAt = Math.ceil(cookieValue.length / 5);
+ const mockRequest = httpServerMock.createRawRequest({
+ state: {
+ [cookiePrefix + '1']: cookieValue.substring(0, splitValueAt),
+ [cookiePrefix + '2']: cookieValue.substring(splitValueAt),
+ },
+ });
+ OpenSearchDashboardsRequest.from(mockRequest);
+ const cookie: SecuritySessionCookie = {
+ credentials: {
+ authHeaderValueExtra: true,
+ },
+ };
+
+ const expectedHeaders = {
+ authorization: testString,
+ };
+
+ const headers = proxyAuthentication.buildAuthHeaderFromCookie(cookie);
+
+ expect(headers).toEqual(expectedHeaders);
+ });
+});
diff --git a/test/constant.ts b/test/constant.ts
index 713ea05de..75e02fe43 100644
--- a/test/constant.ts
+++ b/test/constant.ts
@@ -26,3 +26,6 @@ export const ADMIN_PASSWORD: string = 'admin';
const ADMIN_USER_PASS: string = `${ADMIN_USER}:${ADMIN_PASSWORD}`;
export const ADMIN_CREDENTIALS: string = `Basic ${Buffer.from(ADMIN_USER_PASS).toString('base64')}`;
export const AUTHORIZATION_HEADER_NAME: string = 'Authorization';
+export const PROXY_USER: string = 'x-proxy-user';
+export const PROXY_ROLE: string = 'x-proxy-roles';
+export const PROXY_ADMIN_ROLE: string = 'admin';
diff --git a/test/jest_integration/proxy_auth.test.ts b/test/jest_integration/proxy_auth.test.ts
new file mode 100644
index 000000000..8772bac99
--- /dev/null
+++ b/test/jest_integration/proxy_auth.test.ts
@@ -0,0 +1,101 @@
+/*
+ * Copyright OpenSearch Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import * as osdTestServer from '../../../../src/core/test_helpers/osd_server';
+import { Root } from '../../../../src/core/server/root';
+import { resolve } from 'path';
+import { describe, expect, it, beforeAll, afterAll } from '@jest/globals';
+import {
+ OPENSEARCH_DASHBOARDS_SERVER_USER,
+ OPENSEARCH_DASHBOARDS_SERVER_PASSWORD,
+ ADMIN_USER,
+ PROXY_USER,
+ PROXY_ROLE,
+ PROXY_ADMIN_ROLE,
+} from '../constant';
+
+describe('start OpenSearch Dashboards server', () => {
+ let root: Root;
+
+ beforeAll(async () => {
+ root = osdTestServer.createRootWithSettings(
+ {
+ plugins: {
+ scanDirs: [resolve(__dirname, '../..')],
+ },
+ opensearch: {
+ hosts: ['https://localhost:9200'],
+ ignoreVersionMismatch: true,
+ ssl: { verificationMode: 'none' },
+ username: OPENSEARCH_DASHBOARDS_SERVER_USER,
+ password: OPENSEARCH_DASHBOARDS_SERVER_PASSWORD,
+ requestHeadersAllowlist: [
+ 'securitytenant',
+ 'Authorization',
+ 'x-forwarded-for',
+ 'x-proxy-user',
+ 'x-proxy-roles',
+ ],
+ },
+ opensearch_security: {
+ auth: {
+ type: 'proxy',
+ },
+ proxycache: {
+ user_header: 'x-proxy-user',
+ roles_header: 'x-proxy-roles',
+ },
+ },
+ },
+ {
+ // to make ignoreVersionMismatch setting work
+ // can be removed when we have corresponding ES version
+ dev: true,
+ }
+ );
+ });
+
+ afterAll(async () => {
+ // shutdown OpenSearchDashboards server
+ await root.shutdown();
+ });
+
+ it('can access home page with proxy header', async () => {
+ const response = await osdTestServer.request
+ .get(root, 'app/home#/')
+ .set(PROXY_USER, ADMIN_USER)
+ .set(PROXY_ROLE, PROXY_ADMIN_ROLE);
+ expect(response.status).toEqual(200);
+ });
+
+ it('cannot access home page without proxy header', async () => {
+ const response = await osdTestServer.request.get(root, 'app/home#/');
+ expect(response.status).toEqual(401);
+ });
+
+ it('cannot access home page with partial proxy header', async () => {
+ const response = await osdTestServer.request
+ .get(root, 'app/home#/')
+ .set(PROXY_USER, ADMIN_USER);
+ expect(response.status).toEqual(401);
+ });
+
+ it('cannot access home page with partial proxy header2', async () => {
+ const response = await osdTestServer.request
+ .get(root, 'app/home#/')
+ .set(PROXY_ROLE, PROXY_ADMIN_ROLE);
+ expect(response.status).toEqual(401);
+ });
+});
diff --git a/test/jest_integration/proxy_multiauth.test.ts b/test/jest_integration/proxy_multiauth.test.ts
new file mode 100644
index 000000000..97e9f8bb5
--- /dev/null
+++ b/test/jest_integration/proxy_multiauth.test.ts
@@ -0,0 +1,271 @@
+/*
+ * Copyright OpenSearch Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import * as osdTestServer from '../../../../src/core/test_helpers/osd_server';
+import { Root } from '../../../../src/core/server/root';
+import { resolve } from 'path';
+import { describe, expect, it, beforeAll, afterAll } from '@jest/globals';
+import {
+ ADMIN_CREDENTIALS,
+ OPENSEARCH_DASHBOARDS_SERVER_USER,
+ OPENSEARCH_DASHBOARDS_SERVER_PASSWORD, ADMIN_USER, PROXY_ADMIN_ROLE,
+} from '../constant';
+import wreck from '@hapi/wreck';
+import { Builder, By, until } from 'selenium-webdriver';
+import { Options } from 'selenium-webdriver/firefox';
+
+describe('start OpenSearch Dashboards server', () => {
+ let root: Root;
+ let config;
+
+ // XPath Constants
+ const signInBtnXPath = '//*[@id="btn-sign-in"]';
+ // Browser Settings
+ const browser = 'firefox';
+ const options = new Options().headless();
+
+ beforeAll(async () => {
+ root = osdTestServer.createRootWithSettings(
+ {
+ plugins: {
+ scanDirs: [resolve(__dirname, '../..')],
+ },
+ home: { disableWelcomeScreen: true },
+ server: {
+ host: 'localhost',
+ port: 5601,
+ },
+ logging: {
+ silent: true,
+ verbose: false,
+ },
+ opensearch: {
+ hosts: ['https://localhost:9200'],
+ ignoreVersionMismatch: true,
+ ssl: { verificationMode: 'none' },
+ username: OPENSEARCH_DASHBOARDS_SERVER_USER,
+ password: OPENSEARCH_DASHBOARDS_SERVER_PASSWORD,
+ requestHeadersAllowlist: [
+ 'securitytenant',
+ 'Authorization',
+ 'x-forwarded-for',
+ 'x-proxy-user',
+ 'x-proxy-roles',
+ ],
+ },
+ opensearch_security: {
+ auth: {
+ anonymous_auth_enabled: false,
+ type: ['basicauth', 'proxy'],
+ multiple_auth_enabled: true,
+ },
+ proxycache: {
+ user_header: 'x-proxy-user',
+ roles_header: 'x-proxy-roles',
+ },
+ multitenancy: {
+ enabled: true,
+ tenants: {
+ enable_global: true,
+ enable_private: true,
+ preferred: ['Private', 'Global'],
+ },
+ },
+ },
+ },
+ {
+ // to make ignoreVersionMismatch setting work
+ // can be removed when we have corresponding ES version
+ dev: true,
+ }
+ );
+
+ console.log('Starting OpenSearchDashboards server..');
+ await root.setup();
+ await root.start();
+
+ console.log('Starting to Download Flights Sample Data');
+ await wreck.post('http://localhost:5601/api/sample_data/flights', {
+ payload: {},
+ rejectUnauthorized: false,
+ headers: {
+ 'Content-Type': 'application/json',
+ authorization: ADMIN_CREDENTIALS,
+ security_tenant: 'global',
+ },
+ });
+ console.log('Downloaded Sample Data');
+ const getConfigResponse = await wreck.get(
+ 'https://localhost:9200/_plugins/_security/api/securityconfig',
+ {
+ rejectUnauthorized: false,
+ headers: {
+ authorization: ADMIN_CREDENTIALS,
+ },
+ }
+ );
+ const responseBody = (getConfigResponse.payload as Buffer).toString();
+ config = JSON.parse(responseBody).config;
+ const proxyConfig = {
+ http_enabled: true,
+ transport_enabled: true,
+ order: 0,
+ http_authenticator: {
+ challenge: false,
+ type: 'proxy',
+ config: {
+ user_header: 'x-proxy-user',
+ roles_header: 'x-proxy-roles',
+ },
+ },
+ authentication_backend: {
+ type: 'noop',
+ config: {},
+ },
+ };
+ try {
+ config.dynamic!.authc!.proxy_auth_domain = proxyConfig;
+ config.dynamic!.authc!.basic_internal_auth_domain.http_authenticator.challenge = false;
+ config.dynamic!.http!.anonymous_auth_enabled = false;
+ await wreck.put('https://localhost:9200/_plugins/_security/api/securityconfig/config', {
+ payload: config,
+ rejectUnauthorized: false,
+ headers: {
+ 'Content-Type': 'application/json',
+ authorization: ADMIN_CREDENTIALS,
+ },
+ });
+ } catch (error) {
+ console.log('Got an error while updating security config!!', error.stack);
+ fail(error);
+ }
+ });
+
+ afterAll(async () => {
+ console.log('Remove the Sample Data');
+ await wreck
+ .delete('http://localhost:5601/api/sample_data/flights', {
+ rejectUnauthorized: false,
+ headers: {
+ 'Content-Type': 'application/json',
+ authorization: ADMIN_CREDENTIALS,
+ },
+ })
+ .then((value) => {
+ Promise.resolve(value);
+ })
+ .catch((value) => {
+ Promise.resolve(value);
+ });
+ console.log('Remove the Security Config');
+ await wreck
+ .patch('https://localhost:9200/_plugins/_security/api/securityconfig', {
+ payload: [
+ {
+ op: 'remove',
+ path: '/config/dynamic/authc/proxy_auth_domain',
+ },
+ ],
+ rejectUnauthorized: false,
+ headers: {
+ 'Content-Type': 'application/json',
+ authorization: ADMIN_CREDENTIALS,
+ },
+ })
+ .then((value) => {
+ Promise.resolve(value);
+ })
+ .catch((value) => {
+ Promise.resolve(value);
+ });
+ // shutdown OpenSearchDashboards server
+ await root.shutdown();
+ });
+
+ it('Verify Proxy access to dashboards', async () => {
+ console.log('Wreck access home page');
+ await wreck
+ .get('http://localhost:5601/app/home#', {
+ rejectUnauthorized: true,
+ headers: {
+ 'Content-Type': 'application/json',
+ PROXY_USER: ADMIN_USER,
+ PROXY_ROLE: PROXY_ADMIN_ROLE,
+ },
+ })
+ .then((value) => {
+ Promise.resolve(value);
+ })
+ .catch((value) => {
+ Promise.resolve(value);
+ });
+ });
+ it('Login to Dashboards and resume from nextUrl', async () => {
+ const urlWithHash = `http://localhost:5601/app/security-dashboards-plugin#/getstarted`;
+ const loginUrlWithNextUrl = `http://localhost:5601/app/login?nextUrl=%2Fapp%2Fsecurity-dashboards-plugin#/getstarted`;
+ const driver = getDriver(browser, options).build();
+ await driver.manage().deleteAllCookies();
+ await driver.get(loginUrlWithNextUrl);
+ await driver.wait(until.elementsLocated(By.xpath(signInBtnXPath)), 20000);
+ await driver.findElement(By.xpath(signInBtnXPath)).click();
+ // TODO Use a better XPath.
+ await driver.wait(
+ until.elementsLocated(By.xpath('/html/body/div[1]/div/header/div/div[2]')),
+ 20000
+ );
+ const windowHash = await driver.getCurrentUrl();
+ console.log('windowHash: ' + windowHash);
+ expect(windowHash).toEqual(urlWithHash);
+ const cookie = await driver.manage().getCookies();
+ expect(cookie.length).toEqual(3);
+ await driver.manage().deleteAllCookies();
+ await driver.quit();
+ });
+
+ it('Login to Dashboards without nextUrl', async () => {
+ const urlWithoutHash = `http://localhost:5601/app/home#/`;
+ const loginUrl = `http://localhost:5601/app/login`;
+ const driver = getDriver(browser, options).build();
+ await driver.manage().deleteAllCookies();
+ await driver.get(loginUrl);
+ await driver.wait(until.elementsLocated(By.xpath(signInBtnXPath)), 20000);
+ await driver.findElement(By.xpath(signInBtnXPath)).click();
+ // TODO Use a better XPath.
+ await driver.wait(
+ until.elementsLocated(By.xpath('/html/body/div[1]/div/header/div/div[2]')),
+ 20000
+ );
+ await driver.wait(until.elementsLocated(By.css('img[data-test-subj="defaultLogo"]')), 20000);
+ await driver.wait(
+ until.elementsLocated(By.css('section[aria-labelledby="homDataAdd__title"]')),
+ 20000
+ );
+ await driver.wait(
+ until.elementsLocated(By.css('section[aria-labelledby="homDataManage__title"]')),
+ 20000
+ );
+ const windowHash = await driver.getCurrentUrl();
+ console.log('windowHash: ' + windowHash);
+ expect(windowHash).toEqual(urlWithoutHash);
+ const cookie = await driver.manage().getCookies();
+ expect(cookie.length).toEqual(3);
+ await driver.manage().deleteAllCookies();
+ await driver.quit();
+ });
+});
+
+function getDriver(browser: string, options: Options) {
+ return new Builder().forBrowser(browser).setFirefoxOptions(options);
+}