Skip to content

Commit

Permalink
[ Backport 2.3 ] 1054, 1058 and 1088 (#1132)
Browse files Browse the repository at this point in the history
* Fix for Tenancy info getting lost on re-login in SAML Authentication flow #1058
* SAML Integration Tests #1088
* Preserve URL Hash for SAML based login #1054

Signed-off-by: Aniketh Jain <anijainc@amazon.com>
Signed-off-by: Deepak Devarakonda <devardee@amazon.com>
Co-authored-by: Darshit Chanpura <35282393+DarshitChanpura@users.noreply.github.com>
Co-authored-by: Deepak Devarakonda <devardee@amazon.com>
Co-authored-by: Deepak Devarakonda <80896069+devardee@users.noreply.github.com>

(cherry picked from commit a9d10d8)
(cherry picked from commit a4fa35d)
(cherry picked from commit 05649d6)
  • Loading branch information
expani authored Oct 7, 2022
1 parent 741702d commit 417ecbf
Show file tree
Hide file tree
Showing 16 changed files with 563 additions and 27 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
WORKDIR /opensearch/
ENTRYPOINT /docker-host/os-ep.sh
EOF
docker run -d -p 9200:9200 -p 9600:9600 -i opensearch-test:latest
docker run -d --network=host -i opensearch-test:latest
- name: Checkout OpenSearch Dashboard
uses: actions/checkout@v2
Expand Down Expand Up @@ -103,6 +103,7 @@ jobs:
run: |
cd ./OpenSearch-Dashboards
yarn osd bootstrap
node scripts/build_opensearch_dashboards_platform_plugins.js
- name: Run integration tests
run: |
Expand Down
3 changes: 3 additions & 0 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ plugins.security.allow_default_init_securityindex: true
plugins.security.authcz.admin_dn:
- CN=kirk,OU=client,O=client,L=test, C=de

plugins.security.unsupported.restapi.allow_securityconfig_modification: true
plugins.security.audit.type: internal_opensearch
plugins.security.enable_snapshot_restore_privilege: true
plugins.security.check_snapshot_restore_write_privileges: true
Expand Down Expand Up @@ -117,6 +118,8 @@ Next, go to the base directory and run `yarn osd bootstrap` to install any addit
Now, from the base directory and run `yarn start`. This should start dashboard UI successfully. `Cmd+click` the url in the console output (It should look something like `http://0:5601/omf`). Once the page loads, you should be able to log in with user `admin` and password `admin`.
To run selenium based integration tests, download and export the firefox web-driver to your PATH. Also, run `node scripts/build_opensearch_dashboards_platform_plugins.js` or `yarn start` before running the tests. This is essential to generate the bundles.
## Submitting Changes
See [CONTRIBUTING](CONTRIBUTING.md).
Expand Down
12 changes: 8 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,23 @@
"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",
"typescript": "4.0.2",
"gulp-rename": "2.0.0",
"@testing-library/react-hooks": "^7.0.2",
"@types/hapi__wreck": "^15.0.1"
"@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"
},
"dependencies": {
"@hapi/wreck": "^17.1.0",
"@hapi/cryptiles": "5.0.0",
"@hapi/wreck": "^17.1.0",
"html-entities": "1.3.1"
}
}
8 changes: 6 additions & 2 deletions public/apps/account/account-nav-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ export function AccountNavButton(props: {
<EuiListGroupItem
color="subdued"
key="tenant"
label={<EuiText size="xs">{resolveTenantName(props.tenant || '', username)}</EuiText>}
label={
<EuiText size="xs" id="tenantName">
{resolveTenantName(props.tenant || '', username)}
</EuiText>
}
/>
</EuiFlexItem>
</EuiFlexGroup>
Expand Down Expand Up @@ -140,7 +144,7 @@ export function AccountNavButton(props: {
</div>
);
return (
<EuiHeaderSectionItemButton>
<EuiHeaderSectionItemButton id="user-icon-btn">
<EuiPopover
data-test-subj="account-popover"
id="actionsMenu"
Expand Down
22 changes: 18 additions & 4 deletions public/apps/account/log-out-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@
import React from 'react';
import { EuiButtonEmpty } from '@elastic/eui';
import { HttpStart } from 'opensearch-dashboards/public';
import { logout } from './utils';
import { logout, samlLogout } from './utils';

export function LogoutButton(props: {
authType: string;
http: HttpStart;
divider: JSX.Element;
logoutUrl?: string;
}) {
if (props.authType === 'openid' || props.authType === 'saml') {
if (props.authType === 'openid') {
return (
<div>
{props.divider}
<EuiButtonEmpty
data-test-subj="log-out-1"
data-test-subj="log-out-2"
color="danger"
size="xs"
href={`${props.http.basePath.serverBasePath}/auth/logout`}
Expand All @@ -38,14 +38,28 @@ export function LogoutButton(props: {
</EuiButtonEmpty>
</div>
);
} else if (props.authType === 'saml') {
return (
<div>
{props.divider}
<EuiButtonEmpty
data-test-subj="log-out-1"
color="danger"
size="xs"
onClick={() => samlLogout(props.http)}
>
Log out
</EuiButtonEmpty>
</div>
);
} else if (props.authType === 'proxy') {
return <div />;
} else {
return (
<div>
{props.divider}
<EuiButtonEmpty
data-test-subj="log-out-2"
data-test-subj="log-out-3"
color="danger"
size="xs"
onClick={() => logout(props.http, props.logoutUrl)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Account navigation button renders 1`] = `
<EuiHeaderSectionItemButton>
<EuiHeaderSectionItemButton
id="user-icon-btn"
>
<EuiPopover
anchorPosition="downCenter"
button={
Expand Down Expand Up @@ -63,6 +65,7 @@ exports[`Account navigation button renders 1`] = `
key="tenant"
label={
<EuiText
id="tenantName"
size="xs"
>
tenant1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ exports[`Account menu - Log out button renders renders when auth type is OpenId
<div>
<EuiButtonEmpty
color="danger"
data-test-subj="log-out-1"
data-test-subj="log-out-2"
href="/auth/logout"
size="xs"
>
Expand All @@ -20,7 +20,7 @@ exports[`Account menu - Log out button renders renders when auth type is SAML 1`
<EuiButtonEmpty
color="danger"
data-test-subj="log-out-1"
href="/auth/logout"
onClick={[Function]}
size="xs"
>
Log out
Expand All @@ -32,7 +32,7 @@ exports[`Account menu - Log out button renders renders when auth type is not Ope
<div>
<EuiButtonEmpty
color="danger"
data-test-subj="log-out-2"
data-test-subj="log-out-3"
onClick={[Function]}
size="xs"
>
Expand Down
2 changes: 1 addition & 1 deletion public/apps/account/test/log-out-button.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe('Account menu - Log out button', () => {
const component = shallow(
<LogoutButton authType="dummy" http={mockHttpStart} divider={mockDivider} />
);
component.find('[data-test-subj="log-out-2"]').simulate('click');
component.find('[data-test-subj="log-out-3"]').simulate('click');

expect(logout).toBeCalled();
});
Expand Down
6 changes: 6 additions & 0 deletions public/apps/account/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ export async function logout(http: HttpStart, logoutUrl?: string): Promise<void>
logoutUrl || `${http.basePath.serverBasePath}/app/login?nextUrl=${nextUrl}`;
}

export async function samlLogout(http: HttpStart): Promise<void> {
// This will ensure tenancy is picked up from local storage in the next login.
setShouldShowTenantPopup(null);
window.location.href = `${http.basePath.serverBasePath}${API_AUTH_LOGOUT}`;
}

export async function updateNewPassword(
http: HttpStart,
newPassword: string,
Expand Down
137 changes: 132 additions & 5 deletions server/auth/types/saml/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export class SamlAuthRoutes {
validate: validateNextUrl,
})
),
redirectHash: schema.string(),
}),
},
options: {
Expand All @@ -67,6 +68,7 @@ export class SamlAuthRoutes {
saml: {
nextUrl: request.query.nextUrl,
requestId: samlHeader.requestId,
redirectHash: request.query.redirectHash === 'true',
},
};
this.sessionStorageFactory.asScoped(request).set(cookie);
Expand Down Expand Up @@ -95,13 +97,15 @@ export class SamlAuthRoutes {
async (context, request, response) => {
let requestId: string = '';
let nextUrl: string = '/';
let redirectHash: boolean = false;
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`;
redirectHash = cookie.saml?.redirectHash || false;
}
if (!requestId) {
return response.badRequest({
Expand Down Expand Up @@ -143,11 +147,21 @@ export class SamlAuthRoutes {
expiryTime,
};
this.sessionStorageFactory.asScoped(request).set(cookie);
return response.redirected({
headers: {
location: nextUrl,
},
});
if (redirectHash) {
return response.redirected({
headers: {
location: `${
this.coreSetup.http.basePath.serverBasePath
}/auth/saml/redirectUrlFragment?nextUrl=${escape(nextUrl)}`,
},
});
} else {
return response.redirected({
headers: {
location: nextUrl,
},
});
}
} catch (error) {
context.security_plugin.logger.error(
`SAML SP initiated authentication workflow failed: ${error}`
Expand Down Expand Up @@ -215,6 +229,119 @@ export class SamlAuthRoutes {
}
);

// captureUrlFragment is the first route that will be invoked in the SP initiated login.
// This route will execute the captureUrlFragment.js script.
this.coreSetup.http.resources.register(
{
path: '/auth/saml/captureUrlFragment',
validate: {
query: schema.object({
nextUrl: schema.maybe(
schema.string({
validate: validateNextUrl,
})
),
}),
},
options: {
authRequired: false,
},
},
async (context, request, response) => {
this.sessionStorageFactory.asScoped(request).clear();
const serverBasePath = this.coreSetup.http.basePath.serverBasePath;
return response.renderHtml({
body: `
<!DOCTYPE html>
<title>OSD SAML Capture</title>
<link rel="icon" href="data:,">
<script src="${serverBasePath}/auth/saml/captureUrlFragment.js"></script>
`,
});
}
);

// This script will store the URL Hash in browser's local storage.
this.coreSetup.http.resources.register(
{
path: '/auth/saml/captureUrlFragment.js',
validate: false,
options: {
authRequired: false,
},
},
async (context, request, response) => {
this.sessionStorageFactory.asScoped(request).clear();
return response.renderJs({
body: `let samlHash=window.location.hash.toString();
let redirectHash = false;
if (samlHash !== "") {
window.localStorage.removeItem('samlHash');
window.localStorage.setItem('samlHash', samlHash);
redirectHash = true;
}
let params = new URLSearchParams(window.location.search);
let nextUrl = params.get("nextUrl");
finalUrl = "login?nextUrl=" + encodeURIComponent(nextUrl);
finalUrl += "&redirectHash=" + encodeURIComponent(redirectHash);
window.location.replace(finalUrl);
`,
});
}
);

// Once the User is authenticated via the '_opendistro/_security/saml/acs' route,
// the browser will be redirected to '/auth/saml/redirectUrlFragment' route,
// which will execute the redirectUrlFragment.js.
this.coreSetup.http.resources.register(
{
path: '/auth/saml/redirectUrlFragment',
validate: {
query: schema.object({
nextUrl: schema.any(),
}),
},
options: {
authRequired: true,
},
},
async (context, request, response) => {
const serverBasePath = this.coreSetup.http.basePath.serverBasePath;
return response.renderHtml({
body: `
<!DOCTYPE html>
<title>OSD SAML Success</title>
<link rel="icon" href="data:,">
<script src="${serverBasePath}/auth/saml/redirectUrlFragment.js"></script>
`,
});
}
);

// This script will pop the Hash from local storage if it exists.
// And forward the browser to the next url.
this.coreSetup.http.resources.register(
{
path: '/auth/saml/redirectUrlFragment.js',
validate: false,
options: {
authRequired: true,
},
},
async (context, request, response) => {
return response.renderJs({
body: `let samlHash=window.localStorage.getItem('samlHash');
window.localStorage.removeItem('samlHash');
let params = new URLSearchParams(window.location.search);
let nextUrl = params.get("nextUrl");
finalUrl = nextUrl + samlHash;
window.location.replace(finalUrl);
`,
});
}
);

this.router.get(
{
path: `/auth/logout`,
Expand Down
Loading

0 comments on commit 417ecbf

Please sign in to comment.