diff --git a/config/opensearch_dashboards.yml b/config/opensearch_dashboards.yml
index 4d81b0b3be69..0099d86d84f2 100644
--- a/config/opensearch_dashboards.yml
+++ b/config/opensearch_dashboards.yml
@@ -232,3 +232,55 @@
#data_source.encryption.wrappingKeyName: 'changeme'
#data_source.encryption.wrappingKeyNamespace: 'changeme'
#data_source.encryption.wrappingKey: [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
+
+
+# timelion.ui.enabled: true
+# server.name: opensearch-dashboards
+# server.host: "0.0.0.0"
+# opensearch.hosts: http://localhost:9200
+# opensearch.ssl.verificationMode: none
+# opensearch.username: kibanaserver
+# opensearch.password: kibanaserver
+# opensearch.requestHeadersWhitelist: ["securitytenant","Authorization"]
+# # dashboards_security.multitenancy.enabled: true
+# # dashboards_security.multitenancy.tenants.preferred: ["Private", "Global"]
+# dashboards_security.readonly_mode.roles: ["kibana_read_only"]
+# # dashboards_security.auth.type: "saml"
+# server.xsrf.whitelist: [/_plugins/_security/saml/acs,/_opendistro/_security/saml/acs,/_plugins/_security/saml/acs/idpinitiated,/_opendistro/_security/saml/acs/idpinitiated,/_plugins/_security/saml/logout,/_opendistro/_security/saml/logout]
+
+
+
+
+server.host: 0.0.0.0
+server.port: 5601
+opensearch.hosts: ["http://localhost:9200"]
+opensearch.ssl.verificationMode: none
+opensearch.username: "kibanaserver"
+opensearch.password: "kibanaserver"
+opensearch.requestHeadersWhitelist: [ authorization,securitytenant ]
+
+dashboards_security.idp.setting: {
+ "basicauth_opensearch":
+ {
+ "base_redirect_url": "http://localhost:5601"},
+ "oidc_okta":
+ {
+ "base_redirect_url": "http://localhost:5601",
+ "logout_url": "http://localhost:5601/app/login",
+ "connect_url": "https://dev-16628832.okta.com/.well-known/openid-configuration",
+ "client_id": "0oa566po99gotj46m5d7",
+ "client_secret": "4Gy9_NxFS2Xf97t4GRzkoRlyRAsApRwFcM6Zx9WB",
+ "scope": "openid profile email",
+ "verify_hostnames": "false",
+ "refresh_tokens": "false"},
+ "oidc_google":
+ {
+ "base_redirect_url": "http://localhost:5601",
+ "logout_url": "http://localhost:5601/app/login",
+ "connect_url": "https://accounts.google.com/.well-known/openid-configuration",
+ "client_id": "177403260062-qsvknolof1u4qfmv3qtjti45eps7k3qs.apps.googleusercontent.com",
+ "client_secret": "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",
+ "scope": "openid profile email",
+ "verify_hostnames": "false",
+ "refresh_tokens": "false"},
+}
\ No newline at end of file
diff --git a/src/plugins/dashboards_security/common/index.ts b/src/plugins/dashboards_security/common/index.ts
new file mode 100644
index 000000000000..eac3231d6e84
--- /dev/null
+++ b/src/plugins/dashboards_security/common/index.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export const PLUGIN_ID = 'opensearchDashboardsSecurity';
+export const PLUGIN_NAME = 'security-dashboards-plugin';
+
+export const APP_ID_LOGIN = 'login';
+
+export const API_PREFIX = '/api/v1';
+export const CONFIGURATION_API_PREFIX = 'configuration';
+export const API_ENDPOINT_AUTHINFO = API_PREFIX + '/auth/authinfo';
+export const API_ENDPOINT_AUTHTYPE = API_PREFIX + '/auth/type';
+export const LOGIN_PAGE_URI = '/app/' + APP_ID_LOGIN;
+export const API_AUTH_LOGIN = '/auth/login';
+export const API_AUTH_LOGOUT = '/auth/logout';
+export const OPENID_AUTH_LOGIN = '/auth/openid/login';
+export const SAML_AUTH_LOGIN = '/auth/saml/login';
+export const ANONYMOUS_AUTH_LOGIN = '/auth/anonymous';
+export const SAML_AUTH_LOGIN_WITH_FRAGMENT = '/auth/saml/captureUrlFragment?nextUrl=%2F';
+
+export const OPENID_AUTH_LOGOUT = '/auth/openid/logout';
+export const SAML_AUTH_LOGOUT = '/auth/saml/logout';
+export const ANONYMOUS_AUTH_LOGOUT = '/auth/anonymous/logout';
+
+export const AUTH_HEADER_NAME = 'authorization';
+export const AUTH_GRANT_TYPE = 'authorization_code';
+export const AUTH_RESPONSE_TYPE = 'code';
+
+export const jwtKey = '6aff3042-1327-4f3d-82f0-40a157ac4464';
+
+export const idpCert =
+ 'MIIDzzCCAregAwIBAgIUKizt/svOXO4USLQ3spS2Bn507LYwDQYJKoZIhvcNAQEFBQAwQTEMMAoGA1UECgwDQVdTMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxGjAYBgNVBAMMEU9uZUxvZ2luIEFjY291bnQgMB4XDTIzMDExMzIwMTQzNVoXDTI4MDExMzIwMTQzNVowQTEMMAoGA1UECgwDQVdTMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxGjAYBgNVBAMMEU9uZUxvZ2luIEFjY291bnQgMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwTX1daRM90aJmDCWTL3Iuj4GvK2nHRNZoLP9dzscbFJNMIQEXdyREHSVnFO18KWDfwX3gOgvcuijJUk+r5XCf1oJueUNhme/Q8eSHQe1TOhOVPXuI9BxMyPupeKfmFelIylTNvUoCQo2A/dJURRN2rjz4pOoCqadOlgm2So//J8I/JiZVO6S1YleAjWY5VYOMJMq8QKBBMKkmxok+reA36lmvi2JtUZWpZVo62XVcjP9+uOONyXo7O3VEu8Vwezex2sXFyCm699G1aeRCtHQ3yKmhf0Rm0D+RgZKnG+9i6aeJFTXluBqOrz6CtXtW0SV2NKIeK36EcMH1unlG4/VMwIDAQABo4G+MIG7MAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFKsWx05elVPbItGUYA3SBXVehP7VMHwGA1UdIwR1MHOAFKsWx05elVPbItGUYA3SBXVehP7VoUWkQzBBMQwwCgYDVQQKDANBV1MxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEaMBgGA1UEAwwRT25lTG9naW4gQWNjb3VudCCCFCos7f7LzlzuFEi0N7KUtgZ+dOy2MA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOCAQEAh0Kg8BQrOuWO30A6Qj+VL2Ke0/Y96hgdjYxk4zcIwZcIxfb5U733ftF2H0r8RKBYNrWpEmPwa4RnaTqwRaY/pahZ7kznzgMVUMhT9QZe4uNDLu5HgzAuOdhpYk2qv6+GYqcbMNtKPEtTjp0/KwMntgBkn9dPBSiydqojtwh0i2e2rhFh4gBDvuXdHZCcOWCKYm24IOoEI41Q4JIu1jAk6LM3jErcZdx+Lqa9rvSn6jdC6/jwhR1anqqLU9qGIjN99640z/JIOdK8wPei2veLpZbKIDtG/iaSNkdrFhEE1WNXTnnPImQNVgvIT9QdyOLLdzuQ25G3Qraj47JEMm0Xmw==';
+
+export enum AuthType {
+ BASIC = 'basicauth',
+ OIDC = 'oidc',
+ JWT = 'jwt',
+ SAML = 'saml',
+ PROXY = 'proxy',
+ ANONYMOUS = 'anonymous',
+}
diff --git a/src/plugins/dashboards_security/opensearch_dashboards.json b/src/plugins/dashboards_security/opensearch_dashboards.json
new file mode 100644
index 000000000000..76c5e87a3224
--- /dev/null
+++ b/src/plugins/dashboards_security/opensearch_dashboards.json
@@ -0,0 +1,8 @@
+{
+ "id": "dashboardsSecurity",
+ "version": "opensearchDashboards",
+ "configPath": ["dashboards_security"],
+ "requiredPlugins": ["navigation"],
+ "server": true,
+ "ui": true
+}
diff --git a/src/plugins/dashboards_security/package.json b/src/plugins/dashboards_security/package.json
new file mode 100644
index 000000000000..e0f6e971ed32
--- /dev/null
+++ b/src/plugins/dashboards_security/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "dashboards-security",
+ "version": "3.0.0.0",
+ "main": "target/plugins/dashboards-security",
+ "opensearchDashboards": {
+ "version": "3.0.0",
+ "templateVersion": "3.0.0"
+ },
+ "license": "Apache-2.0",
+ "homepage": "https://github.com/opensearch-project/security-dashboards-plugin",
+ "scripts": {
+ "plugin-helpers": "node ../../scripts/plugin_helpers",
+ "osd": "node ../../scripts/osd",
+ "opensearch": "node ../../scripts/opensearch",
+ "build": "yarn plugin-helpers build && node build_tools/rename_zip.js",
+ "start": "node ../../scripts/opensearch-dashboards --dev",
+ "lint:es": "node ../../scripts/eslint",
+ "lint:style": "node ../../scripts/stylelint",
+ "lint": "yarn run lint:es && yarn run lint:style",
+ "pretest:jest_server": "node ./test/jest_integration/runIdpServer.js &",
+ "test:jest_server": "node ./test/run_jest_tests.js --config ./test/jest.config.server.js",
+ "test:jest_ui": "node ./test/run_jest_tests.js --config ./test/jest.config.ui.js"
+ },
+ "devDependencies": {
+ "@elastic/eslint-import-resolver-kibana": "link:../../packages/osd-eslint-import-resolver-opensearch-dashboards",
+ "@testing-library/react-hooks": "^7.0.2",
+ "@types/hapi__wreck": "^15.0.1",
+ "gulp-rename": "2.0.0",
+ "saml-idp": "^1.2.1",
+ "selenium-webdriver": "^4.0.0-alpha.7",
+ "selfsigned": "^2.0.1",
+ "typescript": "4.0.2",
+ "saml-encoder-decoder-js": "1.0.1",
+ "fast-xml-parser": "4.0.15"
+ },
+ "dependencies": {
+ "@hapi/cryptiles": "5.0.0",
+ "@hapi/wreck": "^17.1.0",
+ "html-entities": "1.3.1",
+ "node-saml": "^4.0.0-beta.2",
+ "@node-saml/passport-saml": "4.0.2",
+ "jsonwebtoken": "9.0.0"
+ }
+}
diff --git a/src/plugins/dashboards_security/public/apps/login/_index.scss b/src/plugins/dashboards_security/public/apps/login/_index.scss
new file mode 100644
index 000000000000..5b48bd965e02
--- /dev/null
+++ b/src/plugins/dashboards_security/public/apps/login/_index.scss
@@ -0,0 +1,25 @@
+/*
+ * 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.
+ */
+
+.login-wrapper {
+ margin: 10% auto;
+ width: 350px;
+ padding: 1rem;
+ position: relative;
+}
+
+.btn-login {
+ width: 100%;
+}
diff --git a/src/plugins/dashboards_security/public/apps/login/login-app.tsx b/src/plugins/dashboards_security/public/apps/login/login-app.tsx
new file mode 100644
index 000000000000..bdbd743c850b
--- /dev/null
+++ b/src/plugins/dashboards_security/public/apps/login/login-app.tsx
@@ -0,0 +1,21 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import './_index.scss';
+// @ts-ignore : Component not used
+import React, { Component } from 'react';
+import ReactDOM from 'react-dom';
+import { AppMountParameters, CoreStart } from '../../../../../core/public';
+import { ClientConfigType } from '../../types';
+import { LoginPage } from './login-page';
+
+export function renderApp(
+ coreStart: CoreStart,
+ params: AppMountParameters,
+ config: ClientConfigType
+) {
+ ReactDOM.render(, params.element);
+ return () => ReactDOM.unmountComponentAtNode(params.element);
+}
diff --git a/src/plugins/dashboards_security/public/apps/login/login-page.tsx b/src/plugins/dashboards_security/public/apps/login/login-page.tsx
new file mode 100644
index 000000000000..01bab7cefd2d
--- /dev/null
+++ b/src/plugins/dashboards_security/public/apps/login/login-page.tsx
@@ -0,0 +1,184 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React, { useState } from 'react';
+import {
+ EuiText,
+ EuiFieldText,
+ EuiIcon,
+ EuiSpacer,
+ EuiButton,
+ EuiImage,
+ EuiListGroup,
+ EuiForm,
+ EuiFormRow,
+} from '@elastic/eui';
+import { AuthType } from 'src/plugins/dashboards_security/common';
+import { ESMap } from 'typescript';
+import { map } from 'bluebird';
+import { CoreStart } from '../../../../../core/public';
+import { ClientConfigType } from '../../types';
+import defaultBrandImage from '../../assets/opensearch_logo_h.svg';
+import { validateCurrentPassword } from '../../utils/auth_utils';
+
+interface LoginButtonConfig {
+ buttonname: string;
+ showbrandimage: boolean;
+ brandimage: string;
+ buttonstyle: string;
+}
+
+interface LoginPageDeps {
+ http: CoreStart['http'];
+ config: ClientConfigType;
+}
+
+function redirect(serverBasePath: string) {
+ // navigate to nextUrl
+ const urlParams = new URLSearchParams(window.location.search);
+ let nextUrl = urlParams.get('nextUrl');
+ if (!nextUrl || nextUrl.toLowerCase().includes('//')) {
+ nextUrl = serverBasePath + '/';
+ }
+ window.location.href = nextUrl + window.location.hash;
+}
+
+export function LoginPage(props: LoginPageDeps) {
+ const [username, setUsername] = React.useState('');
+ const [password, setPassword] = React.useState('');
+ const [loginFailed, setloginFailed] = useState(false);
+ const [loginError, setloginError] = useState('');
+ const [usernameValidationFailed, setUsernameValidationFailed] = useState(false);
+ const [passwordValidationFailed, setPasswordValidationFailed] = useState(false);
+
+ let errorLabel: any = null;
+ if (loginFailed) {
+ errorLabel = (
+
+ {loginError}
+
+ );
+ }
+
+ // @ts-ignore : Parameter 'e' implicitly has an 'any' type.
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ // Clear errors
+ setloginFailed(false);
+ setUsernameValidationFailed(false);
+ setPasswordValidationFailed(false);
+
+ // Form validation
+ if (username === '') {
+ setUsernameValidationFailed(true);
+ return;
+ }
+
+ if (password === '') {
+ setPasswordValidationFailed(true);
+ return;
+ }
+ try {
+ await validateCurrentPassword(props.http, username, password);
+ redirect(props.http.basePath.serverBasePath);
+ } catch (error) {
+ setloginFailed(true);
+ setloginError('Invalid username or password. Please try again.');
+ return;
+ }
+ };
+
+ // TODO: Get brand image from server config
+ return (
+
+ {props.config.ui.basicauth.login.showbrandimage && (
+
+ )}
+
+
+ {props.config.ui.basicauth.login.title || 'Please login to OpenSearch Dashboards'}
+
+
+
+ {props.config.ui.basicauth.login.subtitle ||
+ 'If you have forgotten your username or password, please ask your system administrator'}
+
+
+
+
+ }
+ onChange={(e) => setUsername(e.target.value)}
+ value={username}
+ />
+
+
+ }
+ type="password"
+ onChange={(e) => setPassword(e.target.value)}
+ value={password}
+ />
+
+
+
+ Log In
+
+
+
+
+
+
+ Login with OKTA (OIDC)
+
+
+
+
+
+ Login with Google (OIDC)
+
+
+ {errorLabel}
+
+ );
+}
diff --git a/src/plugins/dashboards_security/public/apps/logout/logout-app.tsx b/src/plugins/dashboards_security/public/apps/logout/logout-app.tsx
new file mode 100644
index 000000000000..0d5689a86247
--- /dev/null
+++ b/src/plugins/dashboards_security/public/apps/logout/logout-app.tsx
@@ -0,0 +1,38 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/*
+ * 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 React from 'react';
+import ReactDOM from 'react-dom';
+import { CoreStart } from 'opensearch-dashboards/public';
+import { ClientConfigType } from '../../types';
+import { LogoutPage } from './logout-page';
+
+export async function setupLogoutButton(coreStart: CoreStart, config: ClientConfigType) {
+ coreStart.chrome.navControls.registerRight({
+ order: 2000,
+ mount: (element: HTMLElement) => {
+ ReactDOM.render(
+ ,
+ element
+ );
+ return () => ReactDOM.unmountComponentAtNode(element);
+ },
+ });
+}
diff --git a/src/plugins/dashboards_security/public/apps/logout/logout-page.tsx b/src/plugins/dashboards_security/public/apps/logout/logout-page.tsx
new file mode 100644
index 000000000000..8dac9368d020
--- /dev/null
+++ b/src/plugins/dashboards_security/public/apps/logout/logout-page.tsx
@@ -0,0 +1,34 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/*
+ * 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 React from 'react';
+import { EuiButtonEmpty } from '@elastic/eui';
+import { HttpStart } from 'opensearch-dashboards/public';
+import { logout } from '../../utils/auth_utils';
+
+export function LogoutPage(props: { http: HttpStart; logoutUrl?: string }) {
+ return (
+
+ logout(props.http, props.logoutUrl)}>
+ Log out
+
+
+ );
+}
diff --git a/src/plugins/dashboards_security/public/assets/get_started.svg b/src/plugins/dashboards_security/public/assets/get_started.svg
new file mode 100644
index 000000000000..842164f5c2b3
--- /dev/null
+++ b/src/plugins/dashboards_security/public/assets/get_started.svg
@@ -0,0 +1,78 @@
+
+
\ No newline at end of file
diff --git a/src/plugins/dashboards_security/public/assets/opensearch_logo_h.svg b/src/plugins/dashboards_security/public/assets/opensearch_logo_h.svg
new file mode 100644
index 000000000000..cb329cadfb40
--- /dev/null
+++ b/src/plugins/dashboards_security/public/assets/opensearch_logo_h.svg
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/plugins/dashboards_security/public/index.ts b/src/plugins/dashboards_security/public/index.ts
new file mode 100644
index 000000000000..d6cd9167faf2
--- /dev/null
+++ b/src/plugins/dashboards_security/public/index.ts
@@ -0,0 +1,13 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { PluginInitializerContext } from 'opensearch-dashboards/public';
+import { SecurityPlugin } from './plugin';
+
+export function plugin(initializerContext: PluginInitializerContext) {
+ return new SecurityPlugin(initializerContext);
+}
+
+export { SecurityPluginSetup, SecurityPluginStart } from './types';
diff --git a/src/plugins/dashboards_security/public/plugin.ts b/src/plugins/dashboards_security/public/plugin.ts
new file mode 100644
index 000000000000..144047ec7bfc
--- /dev/null
+++ b/src/plugins/dashboards_security/public/plugin.ts
@@ -0,0 +1,51 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ Plugin,
+ PluginInitializerContext,
+ CoreSetup,
+ CoreStart,
+ AppMountParameters,
+} from 'opensearch-dashboards/public';
+import { ClientConfigType, SecurityPluginSetup, SecurityPluginStart } from './types';
+import { APP_ID_LOGIN, LOGIN_PAGE_URI } from '../common';
+import { setupLogoutButton } from './apps/logout/logout-app';
+
+export class SecurityPlugin implements Plugin {
+ // @ts-ignore : initializerContext not used
+ constructor(private readonly initializerContext: PluginInitializerContext) {}
+
+ public async setup(core: CoreSetup): Promise {
+ const config = this.initializerContext.config.get();
+
+ /* Privilege evaluation:: check the user's permissiion. Regsiter application based on user's permission
+ * This setep need to be implemented
+ */
+ core.application.register({
+ id: APP_ID_LOGIN,
+ title: 'Security',
+ chromeless: true,
+ appRoute: LOGIN_PAGE_URI,
+ mount: async (params: AppMountParameters) => {
+ const { renderApp } = await import('./apps/login/login-app');
+ // @ts-ignore depsStart not used.
+ const [coreStart, depsStart] = await core.getStartServices();
+ return renderApp(coreStart, params, config);
+ },
+ });
+
+ return {};
+ }
+
+ public start(core: CoreStart): SecurityPluginStart {
+ const config = this.initializerContext.config.get();
+ setupLogoutButton(core, config);
+
+ return {};
+ }
+
+ public stop() {}
+}
diff --git a/src/plugins/dashboards_security/public/types.ts b/src/plugins/dashboards_security/public/types.ts
new file mode 100644
index 000000000000..e0b29bb4d37e
--- /dev/null
+++ b/src/plugins/dashboards_security/public/types.ts
@@ -0,0 +1,61 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { map } from 'rxjs/operators';
+
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface SecurityPluginSetup {}
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface SecurityPluginStart {}
+
+export interface AuthInfo {
+ user_name: string;
+ entitlements: {
+ [entitlement: string]: boolean;
+ };
+}
+
+export interface ClientConfigType {
+ readonly_mode: {
+ roles: string[];
+ };
+ ui: {
+ basicauth: {
+ login: {
+ title: string;
+ subtitle: string;
+ showbrandimage: boolean;
+ brandimage: string;
+ buttonstyle: string;
+ };
+ };
+ openid: {
+ login: {
+ buttonname: string;
+ showbrandimage: boolean;
+ brandimage: string;
+ buttonstyle: string;
+ };
+ };
+ saml: {
+ login: {
+ buttonname: string;
+ showbrandimage: boolean;
+ brandimage: string;
+ buttonstyle: string;
+ };
+ };
+ autologout: boolean;
+ backend_configurable: boolean;
+ };
+ auth: {
+ type: string | string[];
+ anonymous_auth_enabled: boolean;
+ logout_url: string;
+ };
+ idp: {
+ setting: typeof map;
+ };
+}
diff --git a/src/plugins/dashboards_security/public/utils/auth_utils.ts b/src/plugins/dashboards_security/public/utils/auth_utils.ts
new file mode 100644
index 000000000000..9ae972ce61b1
--- /dev/null
+++ b/src/plugins/dashboards_security/public/utils/auth_utils.ts
@@ -0,0 +1,40 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { HttpStart } from 'opensearch-dashboards/public';
+import { API_ENDPOINT_AUTHTYPE } from '../../common';
+import { httpGet, httpPost } from './request_utils';
+
+export async function validateCurrentPassword(
+ http: HttpStart,
+ userName: string,
+ currentPassword: string
+): Promise {
+ await httpPost(http, `/auth/basicauth/opensearch/login`, {
+ username: userName,
+ password: currentPassword,
+ });
+}
+
+export async function logout(http: HttpStart, logoutUrl?: string): Promise {
+ const currentAuthType = (await fetchCurrentAuthType(http))?.currentAuthType;
+ const authType = currentAuthType.split('_')[0];
+ const authIdent = currentAuthType.split('_')[1];
+
+ const logoutEndpoint = `/auth/${authType}/${authIdent}/logout`;
+ // console.log("Logout url:: ", logoutEndpoint);
+ await httpGet(http, logoutEndpoint);
+
+ sessionStorage.clear();
+
+ const basePath = http.basePath.serverBasePath ? http.basePath.serverBasePath : '/';
+ const nextUrl = encodeURIComponent(basePath);
+ window.location.href =
+ logoutUrl || `${http.basePath.serverBasePath}/app/login?nextUrl=${nextUrl}`;
+}
+
+export async function fetchCurrentAuthType(http: HttpStart): Promise {
+ return await httpGet(http, API_ENDPOINT_AUTHTYPE);
+}
diff --git a/src/plugins/dashboards_security/public/utils/request_utils.ts b/src/plugins/dashboards_security/public/utils/request_utils.ts
new file mode 100644
index 000000000000..6907ada6c5c3
--- /dev/null
+++ b/src/plugins/dashboards_security/public/utils/request_utils.ts
@@ -0,0 +1,25 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { HttpStart, HttpHandler } from 'opensearch-dashboards/public';
+
+export async function request(requestFunc: HttpHandler, url: string, body?: object): Promise {
+ if (body) {
+ return (await requestFunc(url, { body: JSON.stringify(body) })) as T;
+ }
+ return (await requestFunc(url)) as T;
+}
+
+export async function httpGet(http: HttpStart, url: string): Promise {
+ return await request(http.get, url);
+}
+
+export async function httpPost(http: HttpStart, url: string, body?: object): Promise {
+ return await request(http.post, url, body);
+}
+
+export async function httpDelete(http: HttpStart, url: string): Promise {
+ return await request(http.delete, url);
+}
diff --git a/src/plugins/dashboards_security/server/auth/auth_handler_factory.ts b/src/plugins/dashboards_security/server/auth/auth_handler_factory.ts
new file mode 100644
index 000000000000..835e5a677c7a
--- /dev/null
+++ b/src/plugins/dashboards_security/server/auth/auth_handler_factory.ts
@@ -0,0 +1,42 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { IRouter, CoreSetup, Logger, SessionStorageFactory } from 'opensearch-dashboards/server';
+import { SecuritySessionCookie } from '../session/security_cookie';
+import { SecurityPluginConfigType } from '..';
+import { IAuthenticationType, IAuthHandlerConstructor } from './types/authentication_type';
+import { MultipleAuthentication } from './types';
+
+async function createAuthentication(
+ ctor: IAuthHandlerConstructor,
+ config: SecurityPluginConfigType,
+ sessionStorageFactory: SessionStorageFactory,
+ router: IRouter,
+ coreSetup: CoreSetup,
+ logger: Logger
+): Promise {
+ const authHandler = new ctor('', config, sessionStorageFactory, router, coreSetup, logger);
+ await authHandler.init();
+ return authHandler;
+}
+
+export async function getAuthenticationHandler(
+ router: IRouter,
+ config: SecurityPluginConfigType,
+ core: CoreSetup,
+ securitySessionStorageFactory: SessionStorageFactory,
+ logger: Logger
+): Promise {
+ const authHandlerType: IAuthHandlerConstructor = MultipleAuthentication;
+ const auth: IAuthenticationType = await createAuthentication(
+ authHandlerType,
+ config,
+ securitySessionStorageFactory,
+ router,
+ core,
+ logger
+ );
+ return auth;
+}
diff --git a/src/plugins/dashboards_security/server/auth/types/authentication_type.ts b/src/plugins/dashboards_security/server/auth/types/authentication_type.ts
new file mode 100644
index 000000000000..dc3546460eaa
--- /dev/null
+++ b/src/plugins/dashboards_security/server/auth/types/authentication_type.ts
@@ -0,0 +1,150 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ AuthenticationHandler,
+ SessionStorageFactory,
+ IRouter,
+ CoreSetup,
+ Logger,
+ AuthToolkit,
+ LifecycleResponseFactory,
+ OpenSearchDashboardsRequest,
+ IOpenSearchDashboardsResponse,
+ AuthResult,
+} from 'opensearch-dashboards/server';
+import { SecurityPluginConfigType } from '../..';
+import { SecuritySessionCookie } from '../../session/security_cookie';
+import { authenticate } from '../../utils/auth_util';
+
+export interface IAuthenticationType {
+ authType: string;
+ authHandler: AuthenticationHandler;
+ init: () => Promise;
+}
+
+export type IAuthHandlerConstructor = new (
+ authType: string,
+ config: SecurityPluginConfigType,
+ sessionStorageFactory: SessionStorageFactory,
+ router: IRouter,
+ coreSetup: CoreSetup,
+ logger: Logger
+) => IAuthenticationType;
+
+export abstract class AuthenticationType implements IAuthenticationType {
+ protected static readonly ROUTES_TO_IGNORE: string[] = [
+ '/api/core/capabilities', // FIXME: need to figureout how to bypass this API call
+ '/app/login',
+ ];
+ protected static readonly REST_API_CALL_HEADER = 'osd-xsrf';
+
+ constructor(
+ public readonly authType: string,
+ protected readonly config: SecurityPluginConfigType,
+ protected readonly sessionStorageFactory: SessionStorageFactory,
+ protected readonly router: IRouter,
+ protected readonly coreSetup: CoreSetup,
+ protected readonly logger: Logger
+ ) {}
+
+ public authHandler: AuthenticationHandler = async (request, response, toolkit) => {
+ // if browser request, auth logic is:
+ // 1. check if request includes auth header or paramter(e.g. jwt in url params) is present, if so, authenticate with auth header.
+ // 2. if auth header not present, check if auth cookie is present, if no cookie, send to authentication workflow
+ // 3. verify whether auth cookie is valid, if not valid, send to authentication workflow
+ // 4. if cookie is valid, pass to route handlers
+ const authHeaders = {};
+ let cookie: SecuritySessionCookie | null | undefined;
+ let authInfo: any | undefined;
+ if (this.authNotRequired(request)) {
+ return toolkit.authenticated();
+ }
+
+ if (this.requestIncludesAuthInfo(request)) {
+ try {
+ const additonalAuthHeader = this.getAdditionalAuthHeader(request);
+ Object.assign(authHeaders, additonalAuthHeader);
+ authInfo = authenticate({
+ username: 'admin',
+ password: 'admin',
+ });
+ cookie = this.getCookie(request, authInfo);
+ this.sessionStorageFactory.asScoped(request).set(cookie);
+ } catch (error: any) {
+ return response.unauthorized({
+ body: error.message,
+ });
+ }
+ } else {
+ try {
+ cookie = await this.sessionStorageFactory.asScoped(request).get();
+ } catch (error: any) {
+ this.logger.error(`Error parsing cookie: ${error.message}`);
+ cookie = undefined;
+ }
+ if (!cookie || !(await this.isValidCookie(cookie))) {
+ // clear cookie
+ this.sessionStorageFactory.asScoped(request).clear();
+
+ // send to auth workflow
+ return this.handleUnauthedRequest(request, response, toolkit);
+ }
+
+ // extend session expiration time
+ if (this.config.session.keepalive) {
+ cookie!.expiryTime = Date.now() + this.config.session.ttl;
+ this.sessionStorageFactory.asScoped(request).set(cookie!);
+ }
+ // cookie is valid and build auth header
+ const authHeadersFromCookie = this.buildAuthHeaderFromCookie(cookie!);
+ Object.assign(authHeaders, authHeadersFromCookie);
+ const additonalAuthHeader = this.getAdditionalAuthHeader(request);
+ Object.assign(authHeaders, additonalAuthHeader);
+ }
+
+ return toolkit.authenticated({
+ requestHeaders: authHeaders,
+ });
+ };
+
+ authNotRequired(request: OpenSearchDashboardsRequest): boolean {
+ const pathname = request.url.pathname;
+ if (!pathname) {
+ return false;
+ }
+ // allow requests to ignored routes
+ if (AuthenticationType.ROUTES_TO_IGNORE.includes(pathname!)) {
+ return true;
+ }
+ // allow requests to routes that doesn't require authentication
+ if (this.config.auth.unauthenticated_routes.indexOf(pathname!) > -1) {
+ // TODO: use opensearch-dashboards server user
+ return true;
+ }
+ return false;
+ }
+
+ isPageRequest(request: OpenSearchDashboardsRequest) {
+ const path = request.url.pathname || '/';
+ return path.startsWith('/app/') || path === '/' || path.startsWith('/goto/');
+ }
+
+ // abstract functions for concrete auth types to implement
+ public abstract requestIncludesAuthInfo(request: OpenSearchDashboardsRequest): boolean;
+ public abstract getAdditionalAuthHeader(request: OpenSearchDashboardsRequest): Promise;
+ public abstract getCookie(
+ request: OpenSearchDashboardsRequest,
+ authInfo: any
+ ): SecuritySessionCookie;
+ public abstract isValidCookie(cookie: SecuritySessionCookie): Promise;
+ protected abstract handleUnauthedRequest(
+ request: OpenSearchDashboardsRequest,
+ response: LifecycleResponseFactory,
+ toolkit: AuthToolkit
+ ): IOpenSearchDashboardsResponse | AuthResult;
+ public abstract buildAuthHeaderFromCookie(cookie: SecuritySessionCookie): any;
+ public abstract init(): Promise;
+}
diff --git a/src/plugins/dashboards_security/server/auth/types/basic/basic_auth.ts b/src/plugins/dashboards_security/server/auth/types/basic/basic_auth.ts
new file mode 100644
index 000000000000..3069a1c634f8
--- /dev/null
+++ b/src/plugins/dashboards_security/server/auth/types/basic/basic_auth.ts
@@ -0,0 +1,106 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { OpenSearchDashboardsResponse } from 'opensearch-dashboards/server/http/router';
+import {
+ CoreSetup,
+ SessionStorageFactory,
+ IRouter,
+ OpenSearchDashboardsRequest,
+ Logger,
+ LifecycleResponseFactory,
+ AuthToolkit,
+} from 'opensearch-dashboards/server';
+import { SecurityPluginConfigType } from '../../..';
+import { SecuritySessionCookie } from '../../../session/security_cookie';
+import { BasicAuthRoutes } from './routes';
+import { AuthenticationType } from '../authentication_type';
+import { LOGIN_PAGE_URI } from '../../../../common';
+import { composeNextUrlQueryParam } from '../../../utils/next_url';
+import { AUTH_HEADER_NAME } from '../../../../common';
+
+export class BasicAuthentication extends AuthenticationType {
+ constructor(
+ authType: string,
+ config: SecurityPluginConfigType,
+ sessionStorageFactory: SessionStorageFactory,
+ router: IRouter,
+ coreSetup: CoreSetup,
+ logger: Logger
+ ) {
+ super(authType, config, sessionStorageFactory, router, coreSetup, logger);
+ }
+
+ public async init() {
+ const routes = new BasicAuthRoutes(
+ this.authType,
+ this.router,
+ this.config,
+ this.sessionStorageFactory,
+ this.coreSetup
+ );
+ routes.setupRoutes();
+ }
+
+ requestIncludesAuthInfo(
+ request: OpenSearchDashboardsRequest
+ ): boolean {
+ return request.headers[AUTH_HEADER_NAME] ? true : false;
+ }
+
+ async getAdditionalAuthHeader(
+ request: OpenSearchDashboardsRequest
+ ): Promise {
+ return {};
+ }
+
+ getCookie(request: OpenSearchDashboardsRequest, authInfo: any): SecuritySessionCookie {
+ return {
+ username: authInfo.user_name,
+ credentials: {
+ authHeaderValue: request.headers[AUTH_HEADER_NAME],
+ },
+ authType: this.authType,
+ expiryTime: Date.now() + this.config.session.ttl,
+ };
+ }
+
+ async isValidCookie(cookie: SecuritySessionCookie): Promise {
+ return (
+ cookie.authType === this.authType &&
+ cookie.expiryTime &&
+ cookie.username &&
+ cookie.credentials?.authHeaderValue
+ );
+ }
+
+ handleUnauthedRequest(
+ request: OpenSearchDashboardsRequest,
+ response: LifecycleResponseFactory,
+ toolkit: AuthToolkit
+ ): OpenSearchDashboardsResponse {
+ if (this.isPageRequest(request)) {
+ const nextUrlParam = composeNextUrlQueryParam(
+ request,
+ this.coreSetup.http.basePath.serverBasePath
+ );
+ const redirectLocation = `${this.coreSetup.http.basePath.serverBasePath}${LOGIN_PAGE_URI}?${nextUrlParam}`;
+ return response.redirected({
+ headers: {
+ location: `${redirectLocation}`,
+ },
+ });
+ } else {
+ return response.unauthorized({
+ body: `Authentication required`,
+ });
+ }
+ }
+
+ buildAuthHeaderFromCookie(cookie: SecuritySessionCookie): any {
+ const headers: any = {};
+ Object.assign(headers, { authorization: cookie.credentials?.authHeaderValue });
+ return headers;
+ }
+}
diff --git a/src/plugins/dashboards_security/server/auth/types/basic/routes.ts b/src/plugins/dashboards_security/server/auth/types/basic/routes.ts
new file mode 100755
index 000000000000..85ea91572952
--- /dev/null
+++ b/src/plugins/dashboards_security/server/auth/types/basic/routes.ts
@@ -0,0 +1,132 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/*
+ * 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 { schema } from '@osd/config-schema';
+import { IRouter, SessionStorageFactory, CoreSetup } from 'opensearch-dashboards/server';
+import {
+ SecuritySessionCookie,
+ clearOldVersionCookieValue,
+} from '../../../session/security_cookie';
+import { SecurityPluginConfigType } from '../../..';
+import { LOGIN_PAGE_URI } from '../../../../common';
+import { authenticate } from '../../../utils/auth_util';
+
+export class BasicAuthRoutes {
+ private authProvider: string;
+ constructor(
+ private readonly authType: string,
+ private readonly router: IRouter,
+ private readonly config: SecurityPluginConfigType,
+ private readonly sessionStorageFactory: SessionStorageFactory,
+ private readonly coreSetup: CoreSetup
+ ) {
+ this.authProvider = this.authType.split('_')[1];
+ }
+
+ public setupRoutes() {
+ this.coreSetup.http.resources.register(
+ {
+ path: LOGIN_PAGE_URI,
+ validate: false,
+ options: {
+ authRequired: false,
+ },
+ },
+ async (context, request, response) => {
+ this.sessionStorageFactory.asScoped(request).clear();
+ const clearOldVersionCookie = clearOldVersionCookieValue(this.config);
+ return response.renderAnonymousCoreApp({
+ headers: {
+ 'set-cookie': clearOldVersionCookie,
+ },
+ });
+ }
+ );
+
+ // login using username and password
+ this.router.post(
+ {
+ path: `/auth/basicauth/${this.authProvider}/login`,
+ validate: {
+ body: schema.object({
+ username: schema.string(),
+ password: schema.string(),
+ }),
+ },
+ options: {
+ authRequired: false,
+ },
+ },
+ async (context, request, response) => {
+ try {
+ const user = authenticate({
+ username: request.body.username,
+ password: request.body.password,
+ });
+
+ this.sessionStorageFactory.asScoped(request).clear();
+ const encodedCredentials = Buffer.from(
+ `${request.body.username}:${request.body.password}`
+ ).toString('base64');
+ const sessionStorage: SecuritySessionCookie = {
+ username: user?.username,
+ credentials: {
+ authHeaderValue: `Basic ${encodedCredentials}`,
+ },
+ authType: this.authType,
+ expiryTime: Date.now() + this.config.session.ttl,
+ };
+
+ this.sessionStorageFactory.asScoped(request).set(sessionStorage);
+ await this.sessionStorageFactory.asScoped(request).get();
+ return response.ok({
+ body: {
+ username: user?.username,
+ },
+ });
+ } catch (error: any) {
+ // console.log(`Basic authentication failed: ${error}`);
+ return response.unauthorized({
+ headers: {
+ 'www-authenticate': 'User not found',
+ },
+ });
+ }
+ }
+ );
+
+ // logout
+ this.router.get(
+ {
+ path: `/auth/basicauth/${this.authProvider}/logout`,
+ validate: false,
+ options: {
+ authRequired: false,
+ },
+ },
+ async (context, request, response) => {
+ this.sessionStorageFactory.asScoped(request).clear();
+ return response.ok({
+ body: {},
+ });
+ }
+ );
+ }
+}
diff --git a/src/plugins/dashboards_security/server/auth/types/basic/user_bank.ts b/src/plugins/dashboards_security/server/auth/types/basic/user_bank.ts
new file mode 100644
index 000000000000..4bb169faeeca
--- /dev/null
+++ b/src/plugins/dashboards_security/server/auth/types/basic/user_bank.ts
@@ -0,0 +1,43 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export interface UserSchema {
+ username: string;
+ password?: string;
+ authOption?: string;
+}
+
+export const userProfiles = new Map([
+ [
+ 'admin',
+ {
+ username: 'admin',
+ password: 'admin',
+ authOption: 'basicauth_opensearch',
+ },
+ ],
+ [
+ 'aoguan',
+ {
+ username: 'aoguan',
+ password: 'admin',
+ authOption: 'basicauth_opensearch',
+ },
+ ],
+ [
+ 'aoguan@amazon.com',
+ {
+ username: 'aoguan@amazon.com',
+ authOption: 'oidc_okta',
+ },
+ ],
+ [
+ 'svc.opensearch.auth@gmail.com',
+ {
+ username: 'svc.opensearch.auth@gmail.com',
+ authOption: 'oidc_okta',
+ },
+ ],
+]);
diff --git a/src/plugins/dashboards_security/server/auth/types/index.ts b/src/plugins/dashboards_security/server/auth/types/index.ts
new file mode 100644
index 000000000000..c094badf6d08
--- /dev/null
+++ b/src/plugins/dashboards_security/server/auth/types/index.ts
@@ -0,0 +1,8 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export { BasicAuthentication } from './basic/basic_auth';
+export { OpenIdAuthentication } from './openid/openid_auth';
+export { MultipleAuthentication } from './multiple/multi_auth';
diff --git a/src/plugins/dashboards_security/server/auth/types/multiple/multi_auth.ts b/src/plugins/dashboards_security/server/auth/types/multiple/multi_auth.ts
new file mode 100644
index 000000000000..bad353669a7b
--- /dev/null
+++ b/src/plugins/dashboards_security/server/auth/types/multiple/multi_auth.ts
@@ -0,0 +1,165 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/*
+ * 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 {
+ CoreSetup,
+ SessionStorageFactory,
+ IRouter,
+ OpenSearchDashboardsRequest,
+ Logger,
+ LifecycleResponseFactory,
+ AuthToolkit,
+} from 'opensearch-dashboards/server';
+import { OpenSearchDashboardsResponse } from '../../../../../../src/core/server/http/router';
+import { SecurityPluginConfigType } from '../../..';
+import { AuthenticationType } from '../authentication_type';
+import { ANONYMOUS_AUTH_LOGIN, AuthType, LOGIN_PAGE_URI } from '../../../../common';
+import { composeNextUrlQueryParam } from '../../../utils/next_url';
+import { SecuritySessionCookie } from '../../../session/security_cookie';
+import { BasicAuthentication, OpenIdAuthentication } from '../../types';
+import { getAuthTypes } from '../../../utils/common_util';
+import { MultiAuthRoutes } from './routes';
+
+export class MultipleAuthentication extends AuthenticationType {
+ private authTypes: string[];
+ private authHandlers: Map;
+
+ constructor(
+ authType: string,
+ config: SecurityPluginConfigType,
+ sessionStorageFactory: SessionStorageFactory,
+ router: IRouter,
+ coreSetup: CoreSetup,
+ logger: Logger
+ ) {
+ super(authType, config, sessionStorageFactory, router, coreSetup, logger);
+ this.authTypes = getAuthTypes(this.config);
+ this.authHandlers = new Map();
+ }
+
+ public async init() {
+ const routes = new MultiAuthRoutes(this.router, this.sessionStorageFactory);
+ routes.setupRoutes();
+
+ for (const type of this.authTypes) {
+ const authOptions = type.split('_');
+ switch (authOptions[0]) {
+ case AuthType.BASIC: {
+ const BasicAuth = new BasicAuthentication(
+ type,
+ this.config,
+ this.sessionStorageFactory,
+ this.router,
+ this.coreSetup,
+ this.logger
+ );
+ await BasicAuth.init();
+ this.authHandlers.set(type, BasicAuth);
+ break;
+ }
+ case AuthType.OIDC: {
+ const OidcAuth = new OpenIdAuthentication(
+ type,
+ this.config,
+ this.sessionStorageFactory,
+ this.router,
+ this.coreSetup,
+ this.logger
+ );
+ await OidcAuth.init();
+ this.authHandlers.set(type, OidcAuth);
+ break;
+ }
+ default: {
+ throw new Error(`Unsupported authentication type: ${authOptions[0]}`);
+ }
+ }
+ }
+ }
+
+ // override functions inherited from AuthenticationType
+ requestIncludesAuthInfo(
+ request: OpenSearchDashboardsRequest
+ ): boolean {
+ for (const key of this.authHandlers.keys()) {
+ if (this.authHandlers.get(key)!.requestIncludesAuthInfo(request)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ async getAdditionalAuthHeader(
+ request: OpenSearchDashboardsRequest
+ ): Promise {
+ // To Do: refactor this method to improve the effiency to get cookie, get cookie from input parameter
+ const cookie = await this.sessionStorageFactory.asScoped(request).get();
+ const reqAuthType = cookie?.authType?.toLowerCase();
+
+ if (reqAuthType && this.authHandlers.has(reqAuthType)) {
+ return this.authHandlers.get(reqAuthType)!.getAdditionalAuthHeader(request);
+ } else {
+ return {};
+ }
+ }
+
+ getCookie(request: OpenSearchDashboardsRequest, authInfo: any): SecuritySessionCookie {
+ return {};
+ }
+
+ async isValidCookie(cookie: SecuritySessionCookie): Promise {
+ const reqAuthType = cookie?.authType?.toLowerCase();
+ if (reqAuthType && this.authHandlers.has(reqAuthType)) {
+ return this.authHandlers.get(reqAuthType)!.isValidCookie(cookie);
+ } else {
+ return false;
+ }
+ }
+
+ handleUnauthedRequest(
+ request: OpenSearchDashboardsRequest,
+ response: LifecycleResponseFactory,
+ toolkit: AuthToolkit
+ ): OpenSearchDashboardsResponse {
+ if (this.isPageRequest(request)) {
+ const nextUrlParam = composeNextUrlQueryParam(
+ request,
+ this.coreSetup.http.basePath.serverBasePath
+ );
+
+ return response.redirected({
+ headers: {
+ location: `${this.coreSetup.http.basePath.serverBasePath}${LOGIN_PAGE_URI}?${nextUrlParam}`,
+ },
+ });
+ } else {
+ return response.unauthorized();
+ }
+ }
+
+ buildAuthHeaderFromCookie(cookie: SecuritySessionCookie): any {
+ const reqAuthType = cookie?.authType?.toLowerCase();
+
+ if (reqAuthType && this.authHandlers.has(reqAuthType)) {
+ return this.authHandlers.get(reqAuthType)!.buildAuthHeaderFromCookie(cookie);
+ } else {
+ return {};
+ }
+ }
+}
diff --git a/src/plugins/dashboards_security/server/auth/types/multiple/routes.ts b/src/plugins/dashboards_security/server/auth/types/multiple/routes.ts
new file mode 100644
index 000000000000..62927c699a43
--- /dev/null
+++ b/src/plugins/dashboards_security/server/auth/types/multiple/routes.ts
@@ -0,0 +1,52 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/*
+ * 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 { IRouter, SessionStorageFactory } from 'opensearch-dashboards/server';
+import { SecuritySessionCookie } from '../../../session/security_cookie';
+import { API_ENDPOINT_AUTHTYPE } from '../../../../common';
+
+export class MultiAuthRoutes {
+ constructor(
+ private readonly router: IRouter,
+ private readonly sessionStorageFactory: SessionStorageFactory
+ ) {}
+
+ public setupRoutes() {
+ this.router.get(
+ {
+ path: API_ENDPOINT_AUTHTYPE,
+ validate: false,
+ },
+ async (context, request, response) => {
+ const cookie = await this.sessionStorageFactory.asScoped(request).get();
+ if (!cookie) {
+ return response.badRequest({
+ body: 'Invalid cookie',
+ });
+ }
+ return response.ok({
+ body: {
+ currentAuthType: cookie?.authType,
+ },
+ });
+ }
+ );
+ }
+}
diff --git a/src/plugins/dashboards_security/server/auth/types/openid/openid_auth.ts b/src/plugins/dashboards_security/server/auth/types/openid/openid_auth.ts
new file mode 100644
index 000000000000..b9a5054510df
--- /dev/null
+++ b/src/plugins/dashboards_security/server/auth/types/openid/openid_auth.ts
@@ -0,0 +1,203 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/*
+ * 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 wreck from '@hapi/wreck';
+import {
+ Logger,
+ SessionStorageFactory,
+ CoreSetup,
+ IRouter,
+ OpenSearchDashboardsRequest,
+ LifecycleResponseFactory,
+ AuthToolkit,
+ IOpenSearchDashboardsResponse,
+} from 'opensearch-dashboards/server';
+import { PeerCertificate } from 'tls';
+import { SecurityPluginConfigType } from '../../..';
+import { SecuritySessionCookie } from '../../../session/security_cookie';
+import { OpenIdAuthRoutes } from './routes';
+import { AuthenticationType } from '../authentication_type';
+import { composeNextUrlQueryParam } from '../../../utils/next_url';
+import { LOGIN_PAGE_URI } from '../../../../common';
+import {
+ callTokenEndpoint,
+ createWreckClient,
+ getExpirationDate,
+ getOIDCConfiguration,
+} from '../../../utils/common_util';
+
+export interface OpenIdAuthConfig {
+ authorizationEndpoint?: string;
+ tokenEndpoint?: string;
+ endSessionEndpoint?: string;
+ scope?: string;
+ issuer?: string;
+ authHeaderName?: string;
+}
+
+export interface WreckHttpsOptions {
+ ca?: string | Buffer | Array;
+ checkServerIdentity?: (host: string, cert: PeerCertificate) => Error | undefined;
+}
+
+export class OpenIdAuthentication extends AuthenticationType {
+ private openIdAuthConfig: OpenIdAuthConfig;
+ private wreckClient: typeof wreck;
+ private idpConfig: any;
+ constructor(
+ authType: string,
+ config: SecurityPluginConfigType,
+ sessionStorageFactory: SessionStorageFactory,
+ router: IRouter,
+ core: CoreSetup,
+ logger: Logger
+ ) {
+ super(authType, config, sessionStorageFactory, router, core, logger);
+ this.wreckClient = createWreckClient(this.config);
+ this.openIdAuthConfig = {};
+ this.idpConfig = this.config.idp.setting.get(this.authType);
+ }
+
+ public async init() {
+ try {
+ await getOIDCConfiguration(
+ this.authType,
+ this.config,
+ this.wreckClient,
+ this.openIdAuthConfig
+ );
+ // console.log('this.openIdAuthConfig:: ', this.openIdAuthConfig);
+
+ const routes = new OpenIdAuthRoutes(
+ this.authType,
+ this.router,
+ this.config,
+ this.sessionStorageFactory,
+ this.openIdAuthConfig,
+ this.coreSetup,
+ this.wreckClient
+ );
+ routes.setupRoutes();
+ } catch (error: any) {
+ this.logger.error(error); // TODO: log more info
+ throw new Error('Failed when trying to obtain the endpoints from your IdP');
+ }
+ }
+
+ requestIncludesAuthInfo(request: OpenSearchDashboardsRequest): boolean {
+ return request.headers.authorization ? true : false;
+ }
+
+ async getAdditionalAuthHeader(request: OpenSearchDashboardsRequest): Promise {
+ return {};
+ }
+
+ getCookie(request: OpenSearchDashboardsRequest, authInfo: any): SecuritySessionCookie {
+ return {
+ username: authInfo.user_name,
+ credentials: {
+ authHeaderValue: request.headers.authorization,
+ },
+ authType: this.authType,
+ expiryTime: Date.now() + this.config.session.ttl,
+ };
+ }
+
+ // TODO: Add token expiration check here
+ async isValidCookie(cookie: SecuritySessionCookie): Promise {
+ if (
+ cookie.authType !== this.authType ||
+ !cookie.username ||
+ !cookie.expiryTime ||
+ !cookie.credentials?.authHeaderValue ||
+ !cookie.credentials?.expires_at
+ ) {
+ return false;
+ }
+ if (cookie.credentials?.expires_at > Date.now()) {
+ return true;
+ }
+
+ // need to renew id token
+ if (cookie.credentials.refresh_token) {
+ try {
+ const query: any = {
+ grant_type: 'refresh_token',
+ client_id: this.idpConfig.client_id,
+ client_secret: this.idpConfig.client_secret,
+ refresh_token: cookie.credentials.refresh_token,
+ };
+ const refreshTokenResponse = await callTokenEndpoint(
+ this.openIdAuthConfig.tokenEndpoint!,
+ query,
+ this.wreckClient
+ );
+
+ // if no id_token from refresh token call, maybe the Idp doesn't allow refresh id_token
+ if (refreshTokenResponse.idToken) {
+ cookie.credentials = {
+ authHeaderValue: `Bearer ${refreshTokenResponse.idToken}`,
+ refresh_token: refreshTokenResponse.refreshToken,
+ expires_at: getExpirationDate(refreshTokenResponse), // expiresIn is in second
+ };
+ return true;
+ } else {
+ return false;
+ }
+ } catch (error: any) {
+ this.logger.error(error);
+ return false;
+ }
+ } else {
+ // no refresh token, and current token is expired
+ return false;
+ }
+ }
+
+ handleUnauthedRequest(
+ request: OpenSearchDashboardsRequest,
+ response: LifecycleResponseFactory,
+ toolkit: AuthToolkit
+ ): IOpenSearchDashboardsResponse {
+ if (this.isPageRequest(request)) {
+ // nextUrl is a key value pair
+ const nextUrl = composeNextUrlQueryParam(
+ request,
+ this.coreSetup.http.basePath.serverBasePath
+ );
+ return response.redirected({
+ headers: {
+ location: `${this.coreSetup.http.basePath.serverBasePath}${LOGIN_PAGE_URI}?${nextUrl}`,
+ },
+ });
+ } else {
+ return response.unauthorized();
+ }
+ }
+
+ buildAuthHeaderFromCookie(cookie: SecuritySessionCookie): any {
+ const header: any = {};
+ const authHeaderValue = cookie.credentials?.authHeaderValue;
+ if (authHeaderValue) {
+ header.authorization = authHeaderValue;
+ }
+ return header;
+ }
+}
diff --git a/src/plugins/dashboards_security/server/auth/types/openid/routes.ts b/src/plugins/dashboards_security/server/auth/types/openid/routes.ts
new file mode 100644
index 000000000000..02c69e8a8891
--- /dev/null
+++ b/src/plugins/dashboards_security/server/auth/types/openid/routes.ts
@@ -0,0 +1,240 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/*
+ * 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 { schema } from '@osd/config-schema';
+import { randomString } from '@hapi/cryptiles';
+import { stringify } from 'querystring';
+import wreck from '@hapi/wreck';
+import {
+ IRouter,
+ SessionStorageFactory,
+ CoreSetup,
+ OpenSearchDashboardsResponseFactory,
+ OpenSearchDashboardsRequest,
+} from 'opensearch-dashboards/server';
+import { SecuritySessionCookie } from '../../../session/security_cookie';
+import { SecurityPluginConfigType } from '../../..';
+import { OpenIdAuthConfig } from './openid_auth';
+import { validateNextUrl } from '../../../utils/next_url';
+import { AUTH_GRANT_TYPE, AUTH_RESPONSE_TYPE } from '../../../../common';
+import { authenticateWithToken } from '../../../utils/auth_util';
+import {
+ callTokenEndpoint,
+ composeLogoutUrl,
+ getBaseRedirectUrl,
+ getExpirationDate,
+} from '../../../utils/common_util';
+
+export class OpenIdAuthRoutes {
+ private static readonly NONCE_LENGTH: number = 22;
+ private authProvider: string;
+ private idpConfig: any;
+
+ constructor(
+ private readonly authType: string,
+ private readonly router: IRouter,
+ private readonly config: SecurityPluginConfigType,
+ private readonly sessionStorageFactory: SessionStorageFactory,
+ private readonly openIdAuthConfig: OpenIdAuthConfig,
+ private readonly core: CoreSetup,
+ private readonly wreckClient: typeof wreck
+ ) {
+ this.authProvider = authType.split('_')[1];
+ this.idpConfig = this.config.idp.setting.get(this.authType);
+ }
+
+ private redirectToLogin(
+ request: OpenSearchDashboardsRequest,
+ response: OpenSearchDashboardsResponseFactory
+ ) {
+ this.sessionStorageFactory.asScoped(request).clear();
+ return response.redirected({
+ headers: {
+ location: `${this.core.http.basePath.serverBasePath}/auth/oidc/${this.authProvider}/login`,
+ },
+ });
+ }
+
+ public setupRoutes() {
+ this.router.get(
+ {
+ path: `/auth/oidc/${this.authProvider}/login`,
+ validate: {
+ query: schema.object(
+ {
+ code: schema.maybe(schema.string()),
+ nextUrl: schema.maybe(
+ schema.string({
+ validate: validateNextUrl,
+ })
+ ),
+ state: schema.maybe(schema.string()),
+ refresh: schema.maybe(schema.string()),
+ },
+ {
+ unknowns: 'allow',
+ }
+ ),
+ },
+ options: {
+ authRequired: false,
+ },
+ },
+ async (context, request, response) => {
+ if (!request.query.code) {
+ const nonce = randomString(OpenIdAuthRoutes.NONCE_LENGTH);
+ const query: any = {
+ client_id: this.idpConfig.client_id,
+ response_type: AUTH_RESPONSE_TYPE,
+ responseMode: 'query',
+ redirect_uri: `${getBaseRedirectUrl(this.config, this.core, request)}/auth/oidc/${
+ this.authProvider
+ }/login`,
+ state: nonce,
+ scope: this.openIdAuthConfig.scope,
+ };
+
+ const queryString = stringify(query);
+ const location = `${this.openIdAuthConfig.authorizationEndpoint}?${queryString}`;
+ const cookie: SecuritySessionCookie = {
+ oidc: {
+ state: nonce,
+ nextUrl: request.query.nextUrl || '/',
+ },
+ authType: this.authType,
+ };
+
+ this.sessionStorageFactory.asScoped(request).set(cookie);
+ return response.redirected({
+ headers: {
+ location,
+ },
+ });
+ }
+
+ // Authentication callback
+ // validate state first
+ let cookie;
+ try {
+ cookie = await this.sessionStorageFactory.asScoped(request).get();
+ if (
+ !cookie ||
+ !cookie.oidc?.state ||
+ cookie.oidc.state !== (request.query as any).state
+ ) {
+ // console.log('cookie got expired, need refresh');
+ return this.redirectToLogin(request, response);
+ }
+ } catch (error) {
+ return this.redirectToLogin(request, response);
+ }
+
+ try {
+ const nextUrl: string = cookie.oidc.nextUrl;
+ const clientId = this.idpConfig.client_id;
+ const clientSecret = this.idpConfig.client_secret;
+ const query: any = {
+ grant_type: AUTH_GRANT_TYPE,
+ code: request.query.code,
+ redirect_uri: `${getBaseRedirectUrl(this.config, this.core, request)}/auth/oidc/${
+ this.authProvider
+ }/login`,
+ client_id: clientId,
+ client_secret: clientSecret,
+ };
+ const tokenResponse = await callTokenEndpoint(
+ this.openIdAuthConfig.tokenEndpoint!,
+ query,
+ this.wreckClient
+ );
+ // console.log('tokenResponse:: ', tokenResponse);
+
+ const user = authenticateWithToken(
+ this.openIdAuthConfig.authHeaderName as string,
+ tokenResponse.idToken,
+ this.idpConfig,
+ this.openIdAuthConfig,
+ this.authType
+ );
+
+ // set to cookie
+ const sessionStorage: SecuritySessionCookie = {
+ username: user.username,
+ credentials: {
+ authHeaderValue: `Bearer ${tokenResponse.idToken}`,
+ expires_at: getExpirationDate(tokenResponse),
+ },
+ authType: this.authType,
+ expiryTime: Date.now() + this.config.session.ttl,
+ };
+
+ if (this.idpConfig?.refresh_tokens && tokenResponse.refreshToken) {
+ Object.assign(sessionStorage.credentials, {
+ refresh_token: tokenResponse.refreshToken,
+ });
+ }
+
+ this.sessionStorageFactory.asScoped(request).set(sessionStorage);
+ return response.redirected({
+ headers: {
+ location: nextUrl,
+ },
+ });
+ } catch (error: any) {
+ // console.log(`OpenId authentication failed: ${error}`);
+ if (error.toString().toLowerCase().includes('authentication exception')) {
+ return response.unauthorized();
+ } else {
+ return this.redirectToLogin(request, response);
+ }
+ }
+ }
+ );
+
+ this.router.get(
+ {
+ path: `/auth/oidc/${this.authProvider}/logout`,
+ validate: false,
+ },
+ async (context, request, response) => {
+ const cookie = await this.sessionStorageFactory.asScoped(request).get();
+ this.sessionStorageFactory.asScoped(request).clear();
+
+ // authHeaderValue is the bearer header, e.g. "Bearer "
+ const token = cookie?.credentials.authHeaderValue.split(' ')[1]; // get auth token
+ const nextUrl = getBaseRedirectUrl(this.config, this.core, request);
+ const logoutQueryParams = {
+ post_logout_redirect_uri: `${nextUrl}`,
+ id_token_hint: token,
+ };
+ const endSessionUrl = composeLogoutUrl(
+ this.idpConfig?.logout_url,
+ this.openIdAuthConfig.endSessionEndpoint,
+ logoutQueryParams
+ );
+ return response.redirected({
+ headers: {
+ location: endSessionUrl,
+ },
+ });
+ }
+ );
+ }
+}
diff --git a/src/plugins/dashboards_security/server/auth/types/saml/routes.ts b/src/plugins/dashboards_security/server/auth/types/saml/routes.ts
new file mode 100644
index 000000000000..31fef97d3c59
--- /dev/null
+++ b/src/plugins/dashboards_security/server/auth/types/saml/routes.ts
@@ -0,0 +1,325 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/*
+ * 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 { schema } from '@osd/config-schema';
+import { compileSchema } from 'ajv/dist/compile';
+import { XMLParser } from 'fast-xml-parser';
+import {
+ IRouter,
+ SessionStorageFactory,
+ OpenSearchDashboardsRequest,
+} from '../../../../../../../src/core/server';
+import { SecuritySessionCookie } from '../../../session/security_cookie';
+import { SecurityPluginConfigType } from '../../..';
+import { SecurityClient } from '../../../backend/opensearch_security_client';
+import { CoreSetup } from '../../../../../../../src/core/server';
+import { validateNextUrl } from '../../../utils/next_url';
+import { AuthType, idpCert, SAML_AUTH_LOGIN, SAML_AUTH_LOGOUT } from '../../../../common';
+import { AuthToken } from './utils/AuthToken';
+import { HapiSaml } from './utils/HapiSaml';
+
+export class SamlAuthRoutes {
+ constructor(
+ private readonly router: IRouter,
+ // @ts-ignore: unused variable
+ private readonly config: SecurityPluginConfigType,
+ private readonly sessionStorageFactory: SessionStorageFactory,
+ private readonly securityClient: SecurityClient,
+ private readonly coreSetup: CoreSetup
+ ) {}
+
+ public setupRoutes() {
+ this.router.get(
+ {
+ path: SAML_AUTH_LOGIN,
+ validate: {
+ query: schema.object({
+ nextUrl: schema.maybe(
+ schema.string({
+ validate: validateNextUrl,
+ })
+ ),
+ }),
+ },
+ options: {
+ authRequired: false,
+ },
+ },
+ async (context, request, response) => {
+ if (request.auth.isAuthenticated) {
+ return response.redirected({
+ headers: {
+ location: `${this.coreSetup.http.basePath.serverBasePath}/app/opensearch-dashboards`,
+ },
+ });
+ }
+
+ try {
+ const samlHeader = await this.securityClient.getSamlHeader(request);
+ // const { nextUrl = '/' } = request.query;
+ const cookie: SecuritySessionCookie = {
+ saml: {
+ nextUrl: request.query.nextUrl,
+ requestId: samlHeader.requestId,
+ },
+ };
+ this.sessionStorageFactory.asScoped(request).set(cookie);
+ return response.redirected({
+ headers: {
+ location: samlHeader.location,
+ },
+ });
+ } catch (error) {
+ context.security_plugin.logger.error(`Failed to get saml header: ${error}`);
+ return response.internalError(); // TODO: redirect to error page?
+ }
+ }
+ );
+
+ this.router.post(
+ {
+ path: `/_opendistro/_security/saml/acs`,
+ validate: {
+ body: schema.any(),
+ },
+ options: {
+ authRequired: false,
+ },
+ },
+ async (context, request, response) => {
+ let requestId: string = '';
+ let nextUrl: string = '/';
+ try {
+ const cookie = await this.sessionStorageFactory.asScoped(request).get();
+ if (cookie) {
+ requestId = cookie.saml?.requestId || '';
+ nextUrl =
+ cookie.saml?.nextUrl ||
+ `${this.coreSetup.http.basePath.serverBasePath}/app/opensearch-dashboards`;
+ }
+ if (!requestId) {
+ return response.badRequest({
+ body: 'Invalid requestId',
+ });
+ }
+ } catch (error) {
+ context.security_plugin.logger.error(`Failed to parse cookie: ${error}`);
+ return response.badRequest();
+ }
+
+ try {
+ const authInfo = await this.securityClient.authinfo(request);
+
+ const samlOptions = {
+ // passport saml settings
+
+ saml: {
+ // this should be the same as the assert path in config below
+ callbackUrl: '/auth/saml/login',
+ // logout functionality is untested at this time.
+ logoutCallbackUrl: 'http://localhost/api/sso/v1/notifylogout',
+ logoutUrl:
+ authInfo.sso_logout_url || this.coreSetup.http.basePath.serverBasePath || '/',
+
+ entryPoint: 'https://cgliu.onelogin.com/trust/saml2/http-redirect/slo/1970238',
+ privateKey: '',
+ // IdP Public Signing Key
+ cert: idpCert,
+ issuer: 'one_login',
+ },
+ // hapi-saml-sp settings
+ config: {
+ // public cert provided in metadata
+ signingCert: '',
+ // Plugin Routes
+ routes: {
+ metadata: {
+ path: './utils/metadata.xml',
+ options: {
+ description: 'Fetch SAML metadata',
+ tags: ['api'],
+ },
+ },
+ assert: {
+ path: `/_opendistro/_security/saml/acs`,
+ options: {
+ description: 'SAML login endpoint',
+ tags: ['api'],
+ },
+ },
+ },
+ assertHooks: {
+ // This will get called after your SAML identity provider sends a
+ // POST request back to the assert endpoint specified above (e.g. /login/saml)
+ onResponse: (
+ profile: any,
+ request: any,
+ h: { redirect: (arg0: string) => any }
+ ) => {
+ // your custom handling code goes in here. I can't help much with this,
+ // but you could set a cookie, or generate a JWT and h.redirect() your user to your
+ // front end with that.
+ return h.redirect('https://your.frontend.test');
+ },
+ },
+ },
+ };
+
+ const hapiSaml = new HapiSaml(samlOptions);
+ const saml = hapiSaml.getSamlLib();
+
+ const SAMLResponse = request.body.SAMLResponse;
+ let profile = null;
+ try {
+ profile = (await saml.validatePostResponseAsync({ SAMLResponse })) || {};
+ } catch (error: any) {
+ context.security_plugin.logger.error(`Error while validating SAML response: ${error}`);
+ return response.internalError();
+ }
+
+ if (profile === null) {
+ return response.internalError();
+ }
+
+ const SAML = require('saml-encoder-decoder-js');
+ const xmlParser = new XMLParser();
+ const samlResponse = request.body.SAMLResponse;
+
+ SAML.decodeSamlPost(samlResponse, function (err: string | undefined, xml: any) {
+ if (err) {
+ throw new Error(err);
+ }
+ const jsonObj = xmlParser.parse(xml);
+ const username =
+ jsonObj['samlp:Response']['saml:Assertion']['saml:Subject']['saml:NameID'];
+ });
+
+ const expiryTime = Date.now() + this.config.session.ttl;
+
+ const authToken = new AuthToken(samlResponse);
+ const credentials = authToken.token;
+
+ const cookie: SecuritySessionCookie = {
+ username: 'cgliu@amazon.com',
+ credentials: {
+ authHeaderValue: authToken.token,
+ },
+ authType: AuthType.SAML, // TODO: create constant
+ expiryTime,
+ };
+
+ this.sessionStorageFactory.asScoped(request).set(cookie);
+ return response.redirected({
+ headers: {
+ location: nextUrl,
+ },
+ });
+ } catch (error) {
+ context.security_plugin.logger.error(
+ `SAML SP initiated authentication workflow failed: ${error}`
+ );
+ }
+
+ return response.internalError();
+ }
+ );
+
+ this.router.post(
+ {
+ path: `/_opendistro/_security/saml/acs/idpinitiated`,
+ validate: {
+ body: schema.any(),
+ },
+ options: {
+ authRequired: false,
+ },
+ },
+ async (context, request, response) => {
+ const acsEndpoint = `${this.coreSetup.http.basePath.serverBasePath}/_opendistro/_security/saml/acs/idpinitiated`;
+ try {
+ const credentials = await this.securityClient.authToken(
+ undefined,
+ request.body.SAMLResponse,
+ acsEndpoint
+ );
+ const user = await this.securityClient.authenticateWithHeader(
+ request,
+ 'authorization',
+ credentials.authorization
+ );
+
+ let expiryTime = Date.now() + this.config.session.ttl;
+ const [headerEncoded, payloadEncoded, signature] = credentials.authorization.split('.');
+ if (!payloadEncoded) {
+ context.security_plugin.logger.error('JWT token payload not found');
+ }
+ const tokenPayload = JSON.parse(Buffer.from(payloadEncoded, 'base64').toString());
+ if (tokenPayload.exp) {
+ expiryTime = parseInt(tokenPayload.exp, 10) * 1000;
+ }
+
+ const cookie: SecuritySessionCookie = {
+ username: user.username,
+ credentials: {
+ authHeaderValue: credentials.authorization,
+ },
+ authType: AuthType.SAML, // TODO: create constant
+ expiryTime,
+ };
+ this.sessionStorageFactory.asScoped(request).set(cookie);
+ return response.redirected({
+ headers: {
+ location: `${this.coreSetup.http.basePath.serverBasePath}/app/opensearch-dashboards`,
+ },
+ });
+ } catch (error) {
+ context.security_plugin.logger.error(
+ `SAML IDP initiated authentication workflow failed: ${error}`
+ );
+ }
+ return response.internalError();
+ }
+ );
+
+ this.router.get(
+ {
+ path: SAML_AUTH_LOGOUT,
+ validate: false,
+ },
+ async (context, request, response) => {
+ try {
+ const authInfo = await this.securityClient.authinfo(request);
+ this.sessionStorageFactory.asScoped(request).clear();
+ // TODO: need a default logout page
+ const redirectUrl =
+ authInfo.sso_logout_url || this.coreSetup.http.basePath.serverBasePath || '/';
+ return response.redirected({
+ headers: {
+ location: redirectUrl,
+ },
+ });
+ } catch (error) {
+ context.security_plugin.logger.error(`SAML logout failed: ${error}`);
+ return response.badRequest();
+ }
+ }
+ );
+ }
+}
diff --git a/src/plugins/dashboards_security/server/auth/types/saml/saml_auth.ts b/src/plugins/dashboards_security/server/auth/types/saml/saml_auth.ts
new file mode 100644
index 000000000000..a9f712d08be9
--- /dev/null
+++ b/src/plugins/dashboards_security/server/auth/types/saml/saml_auth.ts
@@ -0,0 +1,136 @@
+/*
+ * 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 { escape } from 'querystring';
+import { CoreSetup } from 'opensearch-dashboards/server';
+import { SecurityPluginConfigType } from '../../..';
+import {
+ SessionStorageFactory,
+ IRouter,
+ ILegacyClusterClient,
+ OpenSearchDashboardsRequest,
+ AuthToolkit,
+ Logger,
+ LifecycleResponseFactory,
+ IOpenSearchDashboardsResponse,
+ AuthResult,
+} from '../../../../../../src/core/server';
+import {
+ SecuritySessionCookie,
+ clearOldVersionCookieValue,
+} from '../../../session/security_cookie';
+import { SamlAuthRoutes } from './routes';
+import { AuthenticationType } from '../authentication_type';
+import { AuthType, jwtKey } from '../../../../common';
+
+export class SamlAuthentication extends AuthenticationType {
+ public static readonly AUTH_HEADER_NAME = 'authorization';
+
+ public readonly type: string = 'saml';
+
+ constructor(
+ config: SecurityPluginConfigType,
+ sessionStorageFactory: SessionStorageFactory,
+ router: IRouter,
+ esClient: ILegacyClusterClient,
+ coreSetup: CoreSetup,
+ logger: Logger
+ ) {
+ super(config, sessionStorageFactory, router, esClient, coreSetup, logger);
+ }
+
+ private generateNextUrl(request: OpenSearchDashboardsRequest): string {
+ const path =
+ this.coreSetup.http.basePath.serverBasePath +
+ (request.url.path || '/app/opensearch-dashboards');
+ return escape(path);
+ }
+
+ private redirectToLoginUri(request: OpenSearchDashboardsRequest, toolkit: AuthToolkit) {
+ const nextUrl = this.generateNextUrl(request);
+ const clearOldVersionCookie = clearOldVersionCookieValue(this.config);
+ return toolkit.redirected({
+ location: `${this.coreSetup.http.basePath.serverBasePath}/auth/saml/login?nextUrl=${nextUrl}`,
+ 'set-cookie': clearOldVersionCookie,
+ });
+ }
+
+ public async init() {
+ const samlAuthRoutes = new SamlAuthRoutes(
+ this.router,
+ this.config,
+ this.sessionStorageFactory,
+ this.coreSetup
+ );
+ samlAuthRoutes.setupRoutes();
+ }
+
+ requestIncludesAuthInfo(request: OpenSearchDashboardsRequest): boolean {
+ return request.headers[SamlAuthentication.AUTH_HEADER_NAME] ? true : false;
+ }
+
+ async getAdditionalAuthHeader(request: OpenSearchDashboardsRequest): Promise {
+ return {};
+ }
+
+ getCookie(request: OpenSearchDashboardsRequest, authInfo: any): SecuritySessionCookie {
+ return {
+ username: authInfo.user_name,
+ credentials: {
+ authHeaderValue: request.headers[SamlAuthentication.AUTH_HEADER_NAME],
+ },
+ authType: AuthType.SAML,
+ expiryTime: Date.now() + this.config.session.ttl,
+ };
+ }
+
+ // Can be improved to check if the token is expiring.
+ async isValidCookie(cookie: SecuritySessionCookie): Promise {
+ // Validate JWT token in cookie
+ var jwt = require('jsonwebtoken');
+ try {
+ const token = cookie.credentials.authHeaderValue;
+ const decodedToken = jwt.verify(token, jwtKey);
+ } catch (error: any) {
+ this.logger.error(`Failed to validate token: ${error}`);
+ // return false;
+ }
+
+ return (
+ cookie.authType === AuthType.SAML &&
+ cookie.username &&
+ cookie.expiryTime &&
+ cookie.credentials?.authHeaderValue
+ );
+ }
+
+ handleUnauthedRequest(
+ request: OpenSearchDashboardsRequest,
+ response: LifecycleResponseFactory,
+ toolkit: AuthToolkit
+ ): IOpenSearchDashboardsResponse | AuthResult {
+ if (this.isPageRequest(request)) {
+ return this.redirectToLoginUri(request, toolkit);
+ } else {
+ return response.unauthorized();
+ }
+ }
+
+ buildAuthHeaderFromCookie(cookie: SecuritySessionCookie): any {
+ const headers: any = {};
+ headers[SamlAuthentication.AUTH_HEADER_NAME] = cookie.credentials?.authHeaderValue;
+ return headers;
+ }
+}
diff --git a/src/plugins/dashboards_security/server/auth/types/saml/utils/AuthToken.ts b/src/plugins/dashboards_security/server/auth/types/saml/utils/AuthToken.ts
new file mode 100644
index 000000000000..ac91f7060302
--- /dev/null
+++ b/src/plugins/dashboards_security/server/auth/types/saml/utils/AuthToken.ts
@@ -0,0 +1,25 @@
+import { X509Certificate } from 'crypto';
+import { XMLParser } from 'fast-xml-parser';
+import { jwtKey } from '../../../../../common'
+import { SamlAuthentication } from '../saml_auth';
+import { SAMLResponse } from './SAMLResponse';
+// import { jsonwebtoken } from 'jsonwebtoken';
+
+export class AuthToken {
+
+ token: any;
+ jwt: any;
+
+ constructor(samlResponse: SAMLResponse) {
+ this.jwt = require('jsonwebtoken');
+ const jwtExpirySeconds = "1d";
+ const user = {
+ "cgliu@amazon.com": "123456",
+ }
+ this.token = this.jwt.sign({ username: "cgliu@amazon.com" }, jwtKey, {
+ algorithm: "HS256",
+ expiresIn: jwtExpirySeconds,
+ })
+ // this.token = "bearer " + this.token;
+ }
+}
\ No newline at end of file
diff --git a/src/plugins/dashboards_security/server/auth/types/saml/utils/HapiSaml.ts b/src/plugins/dashboards_security/server/auth/types/saml/utils/HapiSaml.ts
new file mode 100644
index 000000000000..e7693ed51b92
--- /dev/null
+++ b/src/plugins/dashboards_security/server/auth/types/saml/utils/HapiSaml.ts
@@ -0,0 +1,64 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { SAML } from 'node-saml';
+
+('use strict');
+
+// const Saml = require('passport-saml/lib/node-saml/saml');
+// const Saml = SAML
+
+export class HapiSaml {
+ saml: any;
+ props: {};
+
+ constructor(options: any) {
+ console.log('!!!options');
+ console.log(options);
+
+ this.saml = null;
+ this.props = {};
+ this.load(options);
+ }
+
+ load(options: any) {
+ if (!options.saml) {
+ throw new Error('Missing options.saml');
+ }
+
+ if (!options.config && !options.config.routes) {
+ throw new Error('Missing options.config.routes');
+ }
+
+ if (!options.config.routes.metadata) {
+ throw new Error('Missing options.config.routes.metadata');
+ }
+
+ if (!options.config.routes.assert) {
+ throw new Error('Missing options.config.routes.assert');
+ }
+
+ if (!options.config && !options.config.assertHooks.onRequest) {
+ throw new Error('Missing options.config.assertHooks.onRequest');
+ }
+
+ if (!options.config && !options.config.assertHooks.onResponse) {
+ throw new Error('Missing options.config.assertHooks.onResponse');
+ }
+
+ console.log('777777');
+
+ this.saml = new SAML(options.saml);
+ this.props = Object.assign({}, options.saml);
+ this.props.decryptionCert = options.config.decryptionCert;
+ this.props.signingCert = options.config.signingCert;
+ }
+
+ getSamlLib() {
+ return this.saml;
+ }
+}
+
+exports.HapiSaml = HapiSaml;
diff --git a/src/plugins/dashboards_security/server/auth/types/saml/utils/SAMLResponse.ts b/src/plugins/dashboards_security/server/auth/types/saml/utils/SAMLResponse.ts
new file mode 100644
index 000000000000..21cba518b73b
--- /dev/null
+++ b/src/plugins/dashboards_security/server/auth/types/saml/utils/SAMLResponse.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { X509Certificate } from 'crypto';
+import { XMLParser } from 'fast-xml-parser';
+
+export class SAMLResponse {
+ private acsUrl: string | undefined;
+ private samlResponseDocument: any;
+ private jsonObj: any;
+ private cert: X509Certificate | undefined;
+
+ constructor(request: any) {
+ if (request !== null) {
+ this.acsUrl = 'http://localhost:5601/_opendistro/_security/saml/acs';
+ this.samlResponseDocument = request.body.SAMLResponse;
+ const SAML = require('saml-encoder-decoder-js');
+ const xmlParser = new XMLParser();
+ SAML.decodeSamlPost(this.samlResponseDocument, (err: string | undefined, xml: any) => {
+ if (err) {
+ throw new Error(err);
+ }
+ this.jsonObj = xmlParser.parse(xml);
+ });
+ }
+ }
+
+ public isValid(samlResponse: SAMLResponse) {
+ return true;
+ }
+}
diff --git a/src/plugins/dashboards_security/server/auth/types/saml/utils/metadata.xml b/src/plugins/dashboards_security/server/auth/types/saml/utils/metadata.xml
new file mode 100644
index 000000000000..2a13412db3e8
--- /dev/null
+++ b/src/plugins/dashboards_security/server/auth/types/saml/utils/metadata.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+ MIIDzzCCAregAwIBAgIUKizt/svOXO4USLQ3spS2Bn507LYwDQYJKoZIhvcNAQEF
+BQAwQTEMMAoGA1UECgwDQVdTMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxGjAYBgNV
+BAMMEU9uZUxvZ2luIEFjY291bnQgMB4XDTIzMDExMzIwMTQzNVoXDTI4MDExMzIw
+MTQzNVowQTEMMAoGA1UECgwDQVdTMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxGjAY
+BgNVBAMMEU9uZUxvZ2luIEFjY291bnQgMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAwTX1daRM90aJmDCWTL3Iuj4GvK2nHRNZoLP9dzscbFJNMIQEXdyR
+EHSVnFO18KWDfwX3gOgvcuijJUk+r5XCf1oJueUNhme/Q8eSHQe1TOhOVPXuI9Bx
+MyPupeKfmFelIylTNvUoCQo2A/dJURRN2rjz4pOoCqadOlgm2So//J8I/JiZVO6S
+1YleAjWY5VYOMJMq8QKBBMKkmxok+reA36lmvi2JtUZWpZVo62XVcjP9+uOONyXo
+7O3VEu8Vwezex2sXFyCm699G1aeRCtHQ3yKmhf0Rm0D+RgZKnG+9i6aeJFTXluBq
+Orz6CtXtW0SV2NKIeK36EcMH1unlG4/VMwIDAQABo4G+MIG7MAwGA1UdEwEB/wQC
+MAAwHQYDVR0OBBYEFKsWx05elVPbItGUYA3SBXVehP7VMHwGA1UdIwR1MHOAFKsW
+x05elVPbItGUYA3SBXVehP7VoUWkQzBBMQwwCgYDVQQKDANBV1MxFTATBgNVBAsM
+DE9uZUxvZ2luIElkUDEaMBgGA1UEAwwRT25lTG9naW4gQWNjb3VudCCCFCos7f7L
+zlzuFEi0N7KUtgZ+dOy2MA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0BAQUFAAOC
+AQEAh0Kg8BQrOuWO30A6Qj+VL2Ke0/Y96hgdjYxk4zcIwZcIxfb5U733ftF2H0r8
+RKBYNrWpEmPwa4RnaTqwRaY/pahZ7kznzgMVUMhT9QZe4uNDLu5HgzAuOdhpYk2q
+v6+GYqcbMNtKPEtTjp0/KwMntgBkn9dPBSiydqojtwh0i2e2rhFh4gBDvuXdHZCc
+OWCKYm24IOoEI41Q4JIu1jAk6LM3jErcZdx+Lqa9rvSn6jdC6/jwhR1anqqLU9qG
+IjN99640z/JIOdK8wPei2veLpZbKIDtG/iaSNkdrFhEE1WNXTnnPImQNVgvIT9Qd
+yOLLdzuQ25G3Qraj47JEMm0Xmw==
+
+
+
+
+
+ urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/plugins/dashboards_security/server/auth/user.ts b/src/plugins/dashboards_security/server/auth/user.ts
new file mode 100644
index 000000000000..b3f496105d05
--- /dev/null
+++ b/src/plugins/dashboards_security/server/auth/user.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export interface User {
+ username: string;
+ credentials?: string;
+}
diff --git a/src/plugins/dashboards_security/server/backend/opensearch_security_client.ts b/src/plugins/dashboards_security/server/backend/opensearch_security_client.ts
new file mode 100755
index 000000000000..bee1be56a872
--- /dev/null
+++ b/src/plugins/dashboards_security/server/backend/opensearch_security_client.ts
@@ -0,0 +1,235 @@
+/*
+ * 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 { ILegacyClusterClient, OpenSearchDashboardsRequest } from '../../../../../src/core/server';
+import { User } from '../auth/user';
+
+export class SecurityClient {
+ constructor(private readonly esClient: ILegacyClusterClient) {}
+
+ public async authenticate(request: OpenSearchDashboardsRequest, credentials: any): Promise {
+ const authHeader = Buffer.from(`${credentials.username}:${credentials.password}`).toString(
+ 'base64'
+ );
+ try {
+ const esResponse = await this.esClient
+ .asScoped(request)
+ .callAsCurrentUser('opensearch_security.authinfo', {
+ headers: {
+ authorization: `Basic ${authHeader}`,
+ },
+ });
+ return {
+ username: credentials.username,
+ roles: esResponse.roles,
+ backendRoles: esResponse.backend_roles,
+ tenants: esResponse.tenants,
+ selectedTenant: esResponse.user_requested_tenant,
+ credentials,
+ proxyCredentials: credentials,
+ };
+ } catch (error: any) {
+ throw new Error(error.message);
+ }
+ }
+
+ public async authenticateWithHeader(
+ request: OpenSearchDashboardsRequest,
+ headerName: string,
+ headerValue: string,
+ whitelistedHeadersAndValues: any = {},
+ additionalAuthHeaders: any = {}
+ ): Promise {
+ try {
+ const credentials: any = {
+ headerName,
+ headerValue,
+ };
+ const headers: any = {};
+ if (headerValue) {
+ headers[headerName] = headerValue;
+ }
+
+ // cannot get config elasticsearch.requestHeadersWhitelist from kibana.yml file in new platfrom
+ // meanwhile, do we really need to save all headers in cookie?
+ const esResponse = {
+ user: 'User [name=admin, backend_roles=[admin], requestedTenant=null]',
+ user_name: 'cgliu@amazon.com',
+ user_requested_tenant: null,
+ remote_address: '127.0.0.1:61197',
+ backend_roles: [ 'admin' ],
+ custom_attribute_names: [],
+ roles: [ 'own_index', 'all_access' ],
+ tenants: { global_tenant: true, admin_tenant: true, admin: true },
+ principal: null,
+ peer_certificates: '0',
+ sso_logout_url: 'https://cgliu.onelogin.com/trust/saml2/http-redirect/slo/1970238?SAMLRequest=fVJda8IwFP0rJe%2BxTbt%2BGLRMcBsFpzDHHvYit02qhTRxuSnIfv1ineAGG%2BTp5Jxzzz3JDKFXR74yezO4F%2FkxSHTBqVca%2BXgzJ4PV3AB2yDX0Erlr%2BHbxvOLxJOJHa5xpjCI3kv8VgCit64wmQbWck836YbV5qtY7EFmeQ5TQRBaM3uWipRAXMRV1Clk9bfxJSPAmLXrtnHgrb4A4yEqjA%2B08FMUJjRiN2WuU87TgLHsnwdLv02lwo%2Brg3BF5GDZ71Q0To6Uy%2B05PGtOHzg7ownP%2BODzTqJWis7LxmDIhm%2BbevSBBOTtT%2BDjZllc%2FZRpQB4OOT%2BMomoW3nItg7YuolsGjsT24vxtiEzYinaDtSOWyh04thLASkZRj8Hvo4dOMsb9HXdzLYHZ5zK3n%2Bn0rLeSp3KVtFrO8TWgt68g3mwKdZg2jbZYWeV0ULYj04vNLeQV%2F%2FI3yCw%3D%3D'
+ }
+ // const esResponse = await this.esClient
+ // .asScoped(request)
+ // .callAsCurrentUser('opensearch_security.authinfo', {
+ // headers,
+ // });
+ return {
+ username: esResponse.user_name,
+ roles: esResponse.roles,
+ backendRoles: esResponse.backend_roles,
+ tenants: esResponse.tenants,
+ selectedTenant: esResponse.user_requested_tenant,
+ credentials,
+ };
+ } catch (error: any) {
+ throw new Error(error.message);
+ }
+ }
+
+ public async authenticateWithHeaders(
+ request: OpenSearchDashboardsRequest,
+ additionalAuthHeaders: any = {}
+ ): Promise {
+ try {
+ const esResponse = await this.esClient
+ .asScoped(request)
+ .callAsCurrentUser('opensearch_security.authinfo', {
+ headers: additionalAuthHeaders,
+ });
+ return {
+ username: esResponse.user_name,
+ roles: esResponse.roles,
+ backendRoles: esResponse.backend_roles,
+ tenants: esResponse.tenants,
+ selectedTenant: esResponse.user_requested_tenant,
+ };
+ } catch (error: any) {
+ throw new Error(error.message);
+ }
+ }
+
+ public async authinfo(request: OpenSearchDashboardsRequest, headers: any = {}) {
+ try {
+ // return await this.esClient
+ // .asScoped(request)
+ // .callAsCurrentUser('opensearch_security.authinfo', {
+ // headers,
+ // });
+ return {
+ user: 'User [name=admin, backend_roles=[admin], requestedTenant=null]',
+ user_name: 'cgliu@amazon.com',
+ user_requested_tenant: null,
+ remote_address: '127.0.0.1:61197',
+ backend_roles: [ 'admin' ],
+ custom_attribute_names: [],
+ roles: [ 'own_index', 'all_access' ],
+ tenants: { global_tenant: true, admin_tenant: true, admin: true },
+ principal: null,
+ peer_certificates: '0',
+ sso_logout_url: 'https://cgliu.onelogin.com/trust/saml2/http-redirect/slo/1970238?SAMLRequest=fVJda8IwFP0rJe%2BxTbt%2BGLRMcBsFpzDHHvYit02qhTRxuSnIfv1ineAGG%2BTp5Jxzzz3JDKFXR74yezO4F%2FkxSHTBqVca%2BXgzJ4PV3AB2yDX0Erlr%2BHbxvOLxJOJHa5xpjCI3kv8VgCit64wmQbWck836YbV5qtY7EFmeQ5TQRBaM3uWipRAXMRV1Clk9bfxJSPAmLXrtnHgrb4A4yEqjA%2B08FMUJjRiN2WuU87TgLHsnwdLv02lwo%2Brg3BF5GDZ71Q0To6Uy%2B05PGtOHzg7ownP%2BODzTqJWis7LxmDIhm%2BbevSBBOTtT%2BDjZllc%2FZRpQB4OOT%2BMomoW3nItg7YuolsGjsT24vxtiEzYinaDtSOWyh04thLASkZRj8Hvo4dOMsb9HXdzLYHZ5zK3n%2Bn0rLeSp3KVtFrO8TWgt68g3mwKdZg2jbZYWeV0ULYj04vNLeQV%2F%2FI3yCw%3D%3D'
+ }
+ } catch (error: any) {
+ throw new Error(error.message);
+ }
+ }
+
+ // Multi-tenancy APIs
+ public async getMultitenancyInfo(request: OpenSearchDashboardsRequest) {
+ try {
+ return await this.esClient
+ .asScoped(request)
+ .callAsCurrentUser('opensearch_security.multitenancyinfo');
+ } catch (error: any) {
+ throw new Error(error.message);
+ }
+ }
+
+ public async getTenantInfoWithInternalUser() {
+ try {
+ return this.esClient.callAsInternalUser('opensearch_security.tenantinfo');
+ } catch (error: any) {
+ throw new Error(error.message);
+ }
+ }
+
+ public async getTenantInfo(request: OpenSearchDashboardsRequest) {
+ try {
+ return await this.esClient
+ .asScoped(request)
+ .callAsCurrentUser('opensearch_security.tenantinfo');
+ } catch (error: any) {
+ throw new Error(error.message);
+ }
+ }
+
+ public async getSamlHeader(request: OpenSearchDashboardsRequest) {
+ try {
+ // response is expected to be an error
+ await this.esClient.asScoped(request).callAsCurrentUser('opensearch_security.authinfo');
+ } catch (error: any) {
+ // the error looks like
+ // wwwAuthenticateDirective:
+ // '
+ // X-Security-IdP realm="Open Distro Security"
+ // location="https:///api/saml2/v1/sso?SAMLRequest="
+ // requestId=""
+ // '
+
+ // if (!error.wwwAuthenticateDirective) {
+ // throw error;
+ // }
+
+ try {
+ return {
+ location: 'https://cgliu.onelogin.com/trust/saml2/http-redirect/sso/62513ad7-0fb6-4ef7-8065-c7d48f5ba802?SAMLRequest=fVJNj9owFPwrke%2FGTiAhsQCJLv1AooAWtodekHFewJJjp35O2%2F33NaGrbg%2B7t6fnmfHM6M1QtqYTyz5c7SP86AFD8rs1FsXwMCe9t8JJ1CisbAFFUOKw%2FLoR2YiLzrvglDPkFeV9hkQEH7SzJFmv5mS3%2FbjZfV5vT1Wual5mQKumKuikgTROqqBlDtWkgHPTyIIk38Bj5M5JlIoCiD2sLQZpQ1zxbEx5SrP0yKciK8Q4%2F06SVcyjrQwD6xpCh4IxdTG6HzkLxl20HSnXsuB7DOzmP2M3GPVQaw8q7tCxIsvTsaynlDfn6A6aKS15kVM1rSdlk59lyTOS7P%2B28UHbWtvL%2B0Wc7yAUX47HPd3vDkeSLF%2FKeXAW%2Bxb8AfxPreDpcXM3H70bp6S5OgwiL3jKTq6DKITBO3ZCUL3X4XnIwaRCspjdRjE05Rcv%2Bf9pVBnnM%2FYaM7vfwzYaXq%2F2zmj1nHxyvpXh7TzpKB02uqbNABW9xQ6UbjTUMZYx7teDBxlgTmLRQBK2uP%2F6%2F%2BEt%2FgA%3D',
+ requestId: 'ONELOGIN_95cd082e-9f96-4fe1-9fc6-85e946ebffa6'
+ }
+ // const locationRegExp = /location="(.*?)"/;
+ // const requestIdRegExp = /requestId="(.*?)"/;
+
+ // const locationExecArray = locationRegExp.exec(error.wwwAuthenticateDirective);
+ // const requestExecArray = requestIdRegExp.exec(error.wwwAuthenticateDirective);
+ // if (locationExecArray && requestExecArray) {
+ // return {
+ // location: locationExecArray[1],
+ // requestId: requestExecArray[1],
+ // };
+ // }
+ throw Error('failed parsing SAML config');
+ } catch (parsingError: any) {
+ console.log(parsingError);
+ throw new Error(parsingError);
+ }
+ }
+ throw new Error(`Invalid SAML configuration.`);
+ }
+
+ public async authToken(
+ requestId: string | undefined,
+ samlResponse: any,
+ acsEndpoint: any | undefined = undefined
+ ) {
+ const body = {
+ RequestId: requestId,
+ SAMLResponse: samlResponse,
+ acsEndpoint,
+ };
+ try {
+ return await this.esClient.asScoped().callAsCurrentUser('opensearch_security.authtoken', {
+ body,
+ });
+ } catch (error: any) {
+ console.log(error);
+ throw new Error('failed to get token');
+ }
+ }
+}
diff --git a/src/plugins/dashboards_security/server/configuration/auth_config.ts b/src/plugins/dashboards_security/server/configuration/auth_config.ts
new file mode 100644
index 000000000000..d26ef6ed8cb0
--- /dev/null
+++ b/src/plugins/dashboards_security/server/configuration/auth_config.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export const identityProviders = new Map([
+ [
+ 'basicauth.internal',
+ {
+ authOption: 'basic.internal',
+ },
+ ],
+ [
+ 'oidc.okta',
+ {
+ authOption: 'oidc.okta',
+ },
+ ],
+]);
diff --git a/src/plugins/dashboards_security/server/index.ts b/src/plugins/dashboards_security/server/index.ts
new file mode 100644
index 000000000000..9bc10ab32423
--- /dev/null
+++ b/src/plugins/dashboards_security/server/index.ts
@@ -0,0 +1,236 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+/*
+ * 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 { schema, TypeOf } from '@osd/config-schema';
+import { PluginInitializerContext, PluginConfigDescriptor } from '../../../../src/core/server';
+import { SecurityPlugin } from './plugin';
+
+const validateAuthType = (value: string[]) => {
+ const supportedAuthTypes = [
+ '',
+ 'basic',
+ 'jwt',
+ 'openid',
+ 'saml',
+ 'proxy',
+ 'kerberos',
+ 'proxycache',
+ ];
+
+ value.forEach((authVal) => {
+ if (!supportedAuthTypes.includes(authVal.toLowerCase())) {
+ throw new Error(
+ `Unsupported authentication type: ${authVal}. Allowed auth.type are ${supportedAuthTypes}.`
+ );
+ }
+ });
+};
+
+export const configSchema = schema.object({
+ enabled: schema.boolean({ defaultValue: true }),
+ allow_client_certificates: schema.boolean({ defaultValue: false }),
+ readonly_mode: schema.object({
+ roles: schema.arrayOf(schema.string(), { defaultValue: [] }),
+ }),
+ clusterPermissions: schema.object({
+ include: schema.arrayOf(schema.string(), { defaultValue: [] }),
+ }),
+ indexPermissions: schema.object({
+ include: schema.arrayOf(schema.string(), { defaultValue: [] }),
+ }),
+ disabledTransportCategories: schema.object({
+ exclude: schema.arrayOf(schema.string(), { defaultValue: [] }),
+ }),
+ disabledRestCategories: schema.object({
+ exclude: schema.arrayOf(schema.string(), { defaultValue: [] }),
+ }),
+ cookie: schema.object({
+ secure: schema.boolean({ defaultValue: false }),
+ name: schema.string({ defaultValue: 'security_authentication' }),
+ password: schema.string({ defaultValue: 'security_cookie_default_password', minLength: 32 }),
+ ttl: schema.number({ defaultValue: 60 * 60 * 1000 }),
+ domain: schema.nullable(schema.string()),
+ isSameSite: schema.oneOf(
+ [
+ schema.literal('Strict'),
+ schema.literal('Lax'),
+ schema.literal('None'),
+ schema.literal(false),
+ ],
+ { defaultValue: false }
+ ),
+ }),
+ session: schema.object({
+ ttl: schema.number({ defaultValue: 60 * 60 * 1000 }),
+ keepalive: schema.boolean({ defaultValue: true }),
+ }),
+ auth: schema.object({
+ type: schema.oneOf(
+ [
+ schema.arrayOf(schema.string(), {
+ defaultValue: [''],
+ validate(value: string[]) {
+ if (!value || value.length === 0) {
+ return `Authentication type is not configured properly. At least one authentication type must be selected.`;
+ }
+
+ if (value.length > 1) {
+ const includeBasicAuth = value.find((element) => {
+ return element.toLowerCase() === 'basicauth';
+ });
+
+ if (!includeBasicAuth) {
+ return `Authentication type is not configured properly. basicauth is mandatory.`;
+ }
+ }
+
+ validateAuthType(value);
+ },
+ }),
+ schema.string({
+ defaultValue: '',
+ validate(value: any) {
+ const valArray: string[] = [];
+ valArray.push(value);
+ validateAuthType(valArray);
+ },
+ }),
+ ],
+ { defaultValue: '' }
+ ),
+ anonymous_auth_enabled: schema.boolean({ defaultValue: false }),
+ unauthenticated_routes: schema.arrayOf(schema.string(), {
+ defaultValue: ['/api/reporting/stats'],
+ }),
+ forbidden_usernames: schema.arrayOf(schema.string(), { defaultValue: [] }),
+ logout_url: schema.string({ defaultValue: '' }),
+ multiple_auth_enabled: schema.boolean({ defaultValue: false }),
+ }),
+ basicauth: schema.object({
+ enabled: schema.boolean({ defaultValue: true }),
+ unauthenticated_routes: schema.arrayOf(schema.string(), { defaultValue: [] }),
+ forbidden_usernames: schema.arrayOf(schema.string(), { defaultValue: [] }),
+ header_trumps_session: schema.boolean({ defaultValue: false }),
+ alternative_login: schema.object({
+ headers: schema.arrayOf(schema.string(), { defaultValue: [] }),
+ show_for_parameter: schema.string({ defaultValue: '' }),
+ valid_redirects: schema.arrayOf(schema.string(), { defaultValue: [] }),
+ button_text: schema.string({ defaultValue: 'Log in with provider' }),
+ buttonstyle: schema.string({ defaultValue: '' }),
+ }),
+ loadbalancer_url: schema.maybe(schema.string()),
+ login: schema.object({
+ title: schema.string({ defaultValue: 'Log in to OpenSearch Dashboards' }),
+ subtitle: schema.string({
+ defaultValue:
+ 'If you have forgotten your username or password, contact your system administrator.',
+ }),
+ showbrandimage: schema.boolean({ defaultValue: true }),
+ brandimage: schema.string({ defaultValue: '' }), // TODO: update brand image
+ buttonstyle: schema.string({ defaultValue: '' }),
+ }),
+ }),
+ configuration: schema.object({
+ enabled: schema.boolean({ defaultValue: true }),
+ }),
+ idp: schema.object({
+ setting: schema.mapOf(schema.string(), schema.any(), {
+ defaultValue: { basicauth_opensearch: { base_redirect_url: 'http://localhost:5601' } },
+ }),
+ }),
+ accountinfo: schema.object({
+ enabled: schema.boolean({ defaultValue: false }),
+ }),
+ openid: schema.maybe(
+ schema.object({
+ connect_url: schema.maybe(schema.string()),
+ header: schema.string({ defaultValue: 'Authorization' }),
+ // TODO: test if siblingRef() works here
+ // client_id is required when auth.type is openid
+ client_id: schema.conditional(
+ schema.siblingRef('auth.type'),
+ 'openid',
+ schema.string(),
+ schema.maybe(schema.string())
+ ),
+ client_secret: schema.string({ defaultValue: '' }),
+ scope: schema.string({ defaultValue: 'openid profile email address phone' }),
+ base_redirect_url: schema.string({ defaultValue: '' }),
+ logout_url: schema.string({ defaultValue: '' }),
+ root_ca: schema.string({ defaultValue: '' }),
+ verify_hostnames: schema.boolean({ defaultValue: true }),
+ refresh_tokens: schema.boolean({ defaultValue: true }),
+ trust_dynamic_headers: schema.boolean({ defaultValue: false }),
+ })
+ ),
+ ui: schema.object({
+ basicauth: schema.object({
+ // the login config here is the same as old config `_security.basicauth.login`
+ // Since we are now rendering login page to browser app, so move these config to browser side.
+ login: schema.object({
+ title: schema.string({ defaultValue: 'Log in to OpenSearch Dashboards' }),
+ subtitle: schema.string({
+ defaultValue:
+ 'If you have forgotten your username or password, contact your system administrator.',
+ }),
+ showbrandimage: schema.boolean({ defaultValue: true }),
+ brandimage: schema.string({ defaultValue: '' }),
+ buttonstyle: schema.string({ defaultValue: '' }),
+ }),
+ }),
+ openid: schema.object({
+ login: schema.object({
+ buttonname: schema.string({ defaultValue: 'Log in with single sign-on' }),
+ showbrandimage: schema.boolean({ defaultValue: false }),
+ brandimage: schema.string({ defaultValue: '' }),
+ buttonstyle: schema.string({ defaultValue: '' }),
+ }),
+ }),
+ saml: schema.object({
+ login: schema.object({
+ buttonname: schema.string({ defaultValue: 'Log in with single sign-on' }),
+ showbrandimage: schema.boolean({ defaultValue: false }),
+ brandimage: schema.string({ defaultValue: '' }),
+ buttonstyle: schema.string({ defaultValue: '' }),
+ }),
+ }),
+ autologout: schema.boolean({ defaultValue: true }),
+ backend_configurable: schema.boolean({ defaultValue: true }),
+ }),
+});
+
+export type SecurityPluginConfigType = TypeOf;
+
+export const config: PluginConfigDescriptor = {
+ exposeToBrowser: {
+ enabled: true,
+ auth: true,
+ ui: true,
+ readonly_mode: true,
+ idp: true,
+ },
+ schema: configSchema,
+};
+
+export function plugin(initializerContext: PluginInitializerContext) {
+ return new SecurityPlugin(initializerContext);
+}
+
+export { SecurityPluginSetup, SecurityPluginStart } from './types';
diff --git a/src/plugins/dashboards_security/server/plugin.ts b/src/plugins/dashboards_security/server/plugin.ts
new file mode 100644
index 000000000000..fdec8438097d
--- /dev/null
+++ b/src/plugins/dashboards_security/server/plugin.ts
@@ -0,0 +1,59 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ PluginInitializerContext,
+ CoreSetup,
+ CoreStart,
+ Plugin,
+ Logger,
+ SessionStorageFactory,
+} from 'opensearch-dashboards/server';
+import { first } from 'rxjs/operators';
+import { SecurityPluginSetup, SecurityPluginStart } from './types';
+import { SecurityPluginConfigType } from '.';
+import { SecuritySessionCookie, getSecurityCookieOptions } from './session/security_cookie';
+import { getAuthenticationHandler } from './auth/auth_handler_factory';
+import { IAuthenticationType } from './auth/types/authentication_type';
+
+export class SecurityPlugin implements Plugin {
+ private readonly logger: Logger;
+
+ constructor(private readonly initializerContext: PluginInitializerContext) {
+ this.logger = initializerContext.logger.get();
+ }
+
+ public async setup(core: CoreSetup) {
+ const config$ = this.initializerContext.config.create();
+ const config: SecurityPluginConfigType = await config$.pipe(first()).toPromise();
+
+ const router = core.http.createRouter();
+
+ const securitySessionStorageFactory: SessionStorageFactory = await core.http.createCookieSessionStorageFactory<
+ SecuritySessionCookie
+ >(getSecurityCookieOptions(config));
+
+ // setup auth
+ const auth: IAuthenticationType = await getAuthenticationHandler(
+ router,
+ config,
+ core,
+ securitySessionStorageFactory,
+ this.logger
+ );
+ core.http.registerAuth(auth.authHandler);
+
+ return {
+ config$,
+ };
+ }
+
+ // TODO: add more logs
+ public async start(core: CoreStart) {
+ return {};
+ }
+
+ public stop() {}
+}
diff --git a/src/plugins/dashboards_security/server/session/security_cookie.ts b/src/plugins/dashboards_security/server/session/security_cookie.ts
new file mode 100644
index 000000000000..63bca5e6d83e
--- /dev/null
+++ b/src/plugins/dashboards_security/server/session/security_cookie.ts
@@ -0,0 +1,70 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { SessionStorageCookieOptions } from 'opensearch-dashboards/server';
+import { SecurityPluginConfigType } from '..';
+
+export interface SecuritySessionCookie {
+ // security_authentication
+ username?: string;
+ credentials?: any;
+ authType?: string;
+ assignAuthHeader?: boolean;
+ isAnonymousAuth?: boolean;
+ expiryTime?: number;
+ additionalAuthHeaders?: any;
+
+ // for oidc auth workflow
+ oidc?: any;
+
+ // for Saml auth workflow
+ saml?: {
+ requestId?: string;
+ nextUrl?: string;
+ redirectHash?: boolean;
+ };
+}
+
+export function getSecurityCookieOptions(
+ config: SecurityPluginConfigType
+): SessionStorageCookieOptions {
+ return {
+ name: config.cookie.name,
+ encryptionKey: config.cookie.password,
+ validate: (sessionStorage: SecuritySessionCookie | SecuritySessionCookie[]) => {
+ sessionStorage = sessionStorage as SecuritySessionCookie;
+ if (sessionStorage === undefined) {
+ return { isValid: false, path: '/' };
+ }
+
+ // TODO: with setting redirect attributes to support OIDC and SAML,
+ // we need to do additonal cookie validatin in AuthenticationHandlers.
+ // if SAML fields present
+ if (sessionStorage.saml && sessionStorage.saml.requestId && sessionStorage.saml.nextUrl) {
+ return { isValid: true, path: '/' };
+ }
+
+ // if OIDC fields present
+ if (sessionStorage.oidc) {
+ return { isValid: true, path: '/' };
+ }
+
+ if (sessionStorage.expiryTime === undefined || sessionStorage.expiryTime < Date.now()) {
+ return { isValid: false, path: '/' };
+ }
+ return { isValid: true, path: '/' };
+ },
+ isSecure: config.cookie.secure,
+ sameSite: config.cookie.isSameSite || undefined,
+ };
+}
+
+export function clearOldVersionCookieValue(config: SecurityPluginConfigType): string {
+ if (config.cookie.secure) {
+ return 'security_authentication=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly; Path=/';
+ } else {
+ return 'security_authentication=; Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/';
+ }
+}
diff --git a/src/plugins/dashboards_security/server/types.ts b/src/plugins/dashboards_security/server/types.ts
new file mode 100644
index 000000000000..09f521219923
--- /dev/null
+++ b/src/plugins/dashboards_security/server/types.ts
@@ -0,0 +1,9 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface SecurityPluginSetup {}
+// eslint-disable-next-line @typescript-eslint/no-empty-interface
+export interface SecurityPluginStart {}
diff --git a/src/plugins/dashboards_security/server/utils/auth_util.ts b/src/plugins/dashboards_security/server/utils/auth_util.ts
new file mode 100644
index 000000000000..3504ea8c5281
--- /dev/null
+++ b/src/plugins/dashboards_security/server/utils/auth_util.ts
@@ -0,0 +1,97 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { User } from '../auth/user';
+import { userProfiles } from '../auth/types/basic/user_bank';
+import { OpenIdAuthConfig } from '../auth/types/openid/openid_auth';
+
+export const authenticate = (authBody: any): User | null => {
+ const user = userProfiles.get(authBody.username);
+ try {
+ if (user !== undefined && user.password === authBody.password) {
+ return {
+ username: authBody.username,
+ credentials: authBody.password,
+ };
+ } else {
+ throw new Error('authentication exception:: User not found');
+ }
+ } catch (error: any) {
+ throw new Error(error.message);
+ }
+};
+
+export const authenticateWithToken = (
+ authzHeader: string,
+ idToken: string | undefined,
+ idpConfig: any,
+ openIdAuthConfig: OpenIdAuthConfig,
+ authType: string
+): User => {
+ try {
+ // If IdToke = null => not authenticated
+ // Else: if token payload.email does not exist => insert entry
+ // else: get the user info from identity storage
+
+ const credentials: any = {
+ authzHeader,
+ idToken,
+ };
+
+ if (!idToken) {
+ throw new Error('authentication exception');
+ } else {
+ const decodedIdToken = decodeIdToken(idToken);
+ if (!validateIdToken(decodedIdToken, idpConfig, openIdAuthConfig)) {
+ throw new Error('authentication exception:: Invalid ID Token');
+ }
+
+ const username = decodedIdToken.email;
+ const user = userProfiles.get(username);
+
+ if (user) {
+ return {
+ username: user.username,
+ credentials,
+ };
+ } else {
+ // insert into user_bank, need implementation
+ userProfiles.set(username, { username, password: '', authOption: authType });
+ // console.log("userProfile:: ", userProfiles);
+ return {
+ username,
+ credentials,
+ };
+ }
+ }
+ } catch (error: any) {
+ throw new Error(error.message);
+ }
+};
+
+const decodeIdToken = (token: string): any => {
+ const parts = token.toString().split('.');
+ if (parts.length !== 3) {
+ throw new Error('authentication exception:: Invalid token');
+ }
+ const claim = JSON.parse(Buffer.from(parts[1], 'base64').toString());
+
+ return claim;
+};
+
+const validateIdToken = (
+ idToken: any,
+ idpConfig: any,
+ openIdAuthConfig: OpenIdAuthConfig
+): boolean => {
+ if (
+ idToken.aud !== idpConfig.client_id ||
+ idToken.iss !== openIdAuthConfig.issuer ||
+ idToken.exp > Date.now()
+ ) {
+ return false;
+ }
+ return true;
+};
diff --git a/src/plugins/dashboards_security/server/utils/common_util.ts b/src/plugins/dashboards_security/server/utils/common_util.ts
new file mode 100644
index 000000000000..e0ff18b591a0
--- /dev/null
+++ b/src/plugins/dashboards_security/server/utils/common_util.ts
@@ -0,0 +1,255 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import wreck from '@hapi/wreck';
+import { PeerCertificate } from 'tls';
+import * as fs from 'fs';
+import HTTP from 'http';
+import HTTPS from 'https';
+import { parse, stringify } from 'querystring';
+import { CoreSetup } from 'opensearch-dashboards/server';
+import { OpenSearchDashboardsRequest } from 'opensearch-dashboards/server';
+import { SecurityPluginConfigType } from '..';
+import { OpenIdAuthConfig, WreckHttpsOptions } from '../auth/types/openid/openid_auth';
+
+export const getAuthTypes = (config: SecurityPluginConfigType): string[] => {
+ const authTypes: string[] = [];
+ const identityProviders = config.idp.setting;
+
+ for (const authType of identityProviders.keys()) {
+ if (authType) {
+ authTypes.push(authType);
+ } else {
+ // Error handling for auth-type not properly defined.
+ }
+ }
+ // console.log("authTypes:: ", authTypes);
+ return authTypes;
+};
+
+export const createWreckClient = (config: SecurityPluginConfigType): typeof wreck => {
+ const wreckHttpsOption: WreckHttpsOptions = {};
+
+ if (config.openid?.root_ca) {
+ wreckHttpsOption.ca = [fs.readFileSync(config.openid.root_ca)];
+ }
+ if (config.openid?.verify_hostnames === false) {
+ // this.logger.debug(`openId auth 'verify_hostnames' option is off.`);
+ wreckHttpsOption.checkServerIdentity = (host: string, cert: PeerCertificate) => {
+ return undefined;
+ };
+ }
+ if (Object.keys(wreckHttpsOption).length > 0) {
+ return wreck.defaults({
+ agents: {
+ http: new HTTP.Agent(),
+ https: new HTTPS.Agent(wreckHttpsOption),
+ httpsAllowUnauthorized: new HTTPS.Agent({
+ rejectUnauthorized: false,
+ }),
+ },
+ });
+ } else {
+ return wreck;
+ }
+};
+
+// OIDC Authentication Helper Methods
+export const getOIDCConfiguration = async (
+ authType: string,
+ config: SecurityPluginConfigType,
+ wreckClient: typeof wreck,
+ openIdAuthConfig: OpenIdAuthConfig
+) => {
+ const idpSetting = config.idp.setting.get(authType);
+ const authHeaderName = config.openid?.header || '';
+ openIdAuthConfig.authHeaderName = authHeaderName;
+
+ let scope = idpSetting.scope;
+ if (scope.indexOf('openid') < 0) {
+ scope = `openid ${scope}`;
+ }
+ openIdAuthConfig.scope = scope;
+ const openIdConnectUrl = idpSetting.connect_url;
+ const response = await wreckClient.get(openIdConnectUrl);
+ const payload = JSON.parse(response.payload as string);
+
+ openIdAuthConfig.authorizationEndpoint = payload.authorization_endpoint;
+ openIdAuthConfig.tokenEndpoint = payload.token_endpoint;
+ openIdAuthConfig.endSessionEndpoint = payload.end_session_endpoint || undefined;
+ openIdAuthConfig.issuer = payload.issuer;
+};
+
+export function parseTokenResponse(payload: Buffer) {
+ const payloadString = payload.toString();
+ if (payloadString.trim()[0] === '{') {
+ try {
+ return JSON.parse(payloadString);
+ } catch (error) {
+ throw Error(`Invalid JSON payload: ${error}`);
+ }
+ }
+ return parse(payloadString);
+}
+
+export function getRootUrl(
+ config: SecurityPluginConfigType,
+ core: CoreSetup,
+ request: OpenSearchDashboardsRequest
+): string {
+ const host = core.http.getServerInfo().hostname;
+ const port = core.http.getServerInfo().port;
+ let protocol = core.http.getServerInfo().protocol;
+ let httpHost = `${host}:${port}`;
+
+ if (config.openid?.trust_dynamic_headers) {
+ const xForwardedHost = (request.headers['x-forwarded-host'] as string) || undefined;
+ const xForwardedProto = (request.headers['x-forwarded-proto'] as string) || undefined;
+ if (xForwardedHost) {
+ httpHost = xForwardedHost;
+ }
+ if (xForwardedProto) {
+ protocol = xForwardedProto;
+ }
+ }
+
+ return `${protocol}://${httpHost}`;
+}
+
+export function getBaseRedirectUrl(
+ config: SecurityPluginConfigType,
+ core: CoreSetup,
+ request: OpenSearchDashboardsRequest
+): string {
+ if (config.openid?.base_redirect_url) {
+ const baseRedirectUrl = config.openid.base_redirect_url;
+ return baseRedirectUrl.endsWith('/') ? baseRedirectUrl.slice(0, -1) : baseRedirectUrl;
+ }
+
+ const rootUrl = getRootUrl(config, core, request);
+ if (core.http.basePath.serverBasePath) {
+ return `${rootUrl}${core.http.basePath.serverBasePath}`;
+ }
+ return rootUrl;
+}
+
+export async function callTokenEndpoint(
+ tokenEndpoint: string,
+ query: any,
+ wreckClient: typeof wreck
+): Promise {
+ const tokenResponse = await wreckClient.post(tokenEndpoint, {
+ payload: stringify(query),
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ });
+
+ if (
+ !tokenResponse.res?.statusCode ||
+ tokenResponse.res.statusCode < 200 ||
+ tokenResponse.res.statusCode > 299
+ ) {
+ throw new Error(
+ `Failed calling token endpoint: ${tokenResponse.res.statusCode} ${tokenResponse.res.statusMessage}`
+ );
+ }
+ const tokenPayload: any = parseTokenResponse(tokenResponse.payload as Buffer);
+
+ return {
+ idToken: tokenPayload.id_token,
+ accessToken: tokenPayload.access_token,
+ refreshToken: tokenPayload.refresh_token,
+ expiresIn: tokenPayload.expires_in,
+ };
+}
+
+export function composeLogoutUrl(
+ customLogoutUrl: string | undefined,
+ idpEndsessionEndpoint: string | undefined,
+ additionalQueryParams: any
+) {
+ const logoutEndpont = customLogoutUrl || idpEndsessionEndpoint;
+ const logoutUrl = new URL(logoutEndpont!);
+ Object.keys(additionalQueryParams).forEach((key) => {
+ logoutUrl.searchParams.append(key, additionalQueryParams[key] as string);
+ });
+ return logoutUrl.toString();
+}
+
+export interface TokenResponse {
+ idToken?: string;
+ accessToken?: string;
+ refreshToken?: string;
+ expiresIn?: number;
+}
+
+export function getExpirationDate(tokenResponse: TokenResponse | undefined) {
+ if (!tokenResponse) {
+ throw new Error('Invalid token');
+ } else if (tokenResponse.idToken) {
+ const idToken = tokenResponse.idToken;
+ const parts = idToken.split('.');
+ if (parts.length !== 3) {
+ throw new Error('Invalid token');
+ }
+ const claim = JSON.parse(Buffer.from(parts[1], 'base64').toString());
+ return claim.exp * 1000;
+ } else {
+ return Date.now() + tokenResponse.expiresIn! * 1000;
+ }
+}
+
+/*
+export async function callTokenEndpoint(
+ request: OpenSearchDashboardsRequest,
+ core: CoreSetup,
+ cookie: SecuritySessionCookie,
+ config: SecurityPluginConfigType,
+ openIdAuthConfig: OpenIdAuthConfig,
+ wreckClient: typeof wreck
+): Promise {
+ const nextUrl: string = cookie.oidc.nextUrl;
+ const clientId = config.openid?.client_id;
+ const clientSecret = config.openid?.client_secret;
+ const query: any = {
+ grant_type: AUTH_GRANT_TYPE,
+ code: request.query.code,
+ redirect_uri: `${getBaseRedirectUrl(
+ config,
+ core,
+ request
+ )}${OPENID_AUTH_LOGIN}`,
+ client_id: clientId,
+ client_secret: clientSecret,
+ };
+ console.log("callTokenEndpoint::nextUrl:: ", nextUrl);
+ const tokenEndpoint = openIdAuthConfig.tokenEndpoint!;
+ const tokenResponse = await wreckClient.post(tokenEndpoint, {
+ payload: stringify(query),
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ });
+
+ if (
+ !tokenResponse.res?.statusCode ||
+ tokenResponse.res.statusCode < 200 ||
+ tokenResponse.res.statusCode > 299
+ ) {
+ throw new Error(
+ `Failed calling token endpoint: ${tokenResponse.res.statusCode} ${tokenResponse.res.statusMessage}`
+ );
+ }
+ const tokenPayload: any = parseTokenResponse(tokenResponse.payload as Buffer);
+ console.log("tokenPayload:: ", tokenPayload);
+ return {
+ idToken: tokenPayload.id_token,
+ accessToken: tokenPayload.access_token,
+ refreshToken: tokenPayload.refresh_token,
+ expiresIn: tokenPayload.expires_in,
+ };
+}
+*/
diff --git a/src/plugins/dashboards_security/server/utils/next_url.ts b/src/plugins/dashboards_security/server/utils/next_url.ts
new file mode 100644
index 000000000000..05e7b45a8d5c
--- /dev/null
+++ b/src/plugins/dashboards_security/server/utils/next_url.ts
@@ -0,0 +1,59 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { parse } from 'url';
+import { ParsedUrlQuery } from 'querystring';
+import { OpenSearchDashboardsRequest } from 'opensearch-dashboards/server';
+import { encodeUriQuery } from '../../../opensearch_dashboards_utils/common/url/encode_uri_query';
+
+export function composeNextUrlQueryParam(
+ request: OpenSearchDashboardsRequest,
+ basePath: string
+): string {
+ try {
+ const currentUrl = request.url.toString();
+ const parsedUrl = parse(currentUrl, true);
+ const nextUrl = parsedUrl?.path;
+
+ if (!!nextUrl && nextUrl !== '/') {
+ return `nextUrl=${encodeUriQuery(basePath + nextUrl)}`;
+ }
+ } catch (error) {
+ /* Ignore errors from parsing */
+ }
+ return '';
+}
+
+export interface ParsedUrlQueryParams extends ParsedUrlQuery {
+ nextUrl: string;
+}
+
+export const INVALID_NEXT_URL_PARAMETER_MESSAGE = 'Invalid nextUrl parameter.';
+
+/**
+ * We require the nextUrl parameter to be an relative url.
+ *
+ * Here we leverage the normalizeUrl function. If the library can parse the url
+ * parameter, which means it is an absolute url, then we reject it. Otherwise, the
+ * library cannot parse the url, which means it is not an absolute url, we let to
+ * go through.
+ * Note: url has been decoded by OpenSearchDashboards.
+ *
+ * @param url url string.
+ * @returns error message if nextUrl is invalid, otherwise void.
+ */
+export const validateNextUrl = (url: string | undefined): string | void => {
+ if (url) {
+ const path = url.split('?')[0];
+ if (
+ !path.startsWith('/') ||
+ path.startsWith('//') ||
+ path.includes('\\') ||
+ path.includes('@')
+ ) {
+ return INVALID_NEXT_URL_PARAMETER_MESSAGE;
+ }
+ }
+};