From a9d10d82f03aa759d3e51e841d7dbfcc28db5628 Mon Sep 17 00:00:00 2001
From: Deepak Devarakonda <80896069+devardee@users.noreply.github.com>
Date: Fri, 5 Aug 2022 23:25:44 +0530
Subject: [PATCH] Preserve URL Hash for SAML based login (#1039)
* Preserve URL HASH after user logs via SAML IDP
Co-authored-by: Darshit Chanpura <35282393+DarshitChanpura@users.noreply.github.com>
---
server/auth/types/saml/routes.ts | 137 +++++++++++++++++++++++++++-
server/auth/types/saml/saml_auth.ts | 10 +-
server/session/security_cookie.ts | 1 +
3 files changed, 138 insertions(+), 10 deletions(-)
diff --git a/server/auth/types/saml/routes.ts b/server/auth/types/saml/routes.ts
index 808dfa8ae..79454272c 100644
--- a/server/auth/types/saml/routes.ts
+++ b/server/auth/types/saml/routes.ts
@@ -46,6 +46,7 @@ export class SamlAuthRoutes {
validate: validateNextUrl,
})
),
+ redirectHash: schema.string(),
}),
},
options: {
@@ -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);
@@ -95,6 +97,7 @@ 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) {
@@ -102,6 +105,7 @@ export class SamlAuthRoutes {
nextUrl =
cookie.saml?.nextUrl ||
`${this.coreSetup.http.basePath.serverBasePath}/app/opensearch-dashboards`;
+ redirectHash = cookie.saml?.redirectHash || false;
}
if (!requestId) {
return response.badRequest({
@@ -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}`
@@ -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: `
+
+
OSD SAML Capture
+
+
+ `,
+ });
+ }
+ );
+
+ // 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: `
+
+ OSD SAML Success
+
+
+ `,
+ });
+ }
+ );
+
+ // 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`,
diff --git a/server/auth/types/saml/saml_auth.ts b/server/auth/types/saml/saml_auth.ts
index d9e61718b..201e76c43 100644
--- a/server/auth/types/saml/saml_auth.ts
+++ b/server/auth/types/saml/saml_auth.ts
@@ -54,18 +54,18 @@ export class SamlAuthentication extends AuthenticationType {
private generateNextUrl(request: OpenSearchDashboardsRequest): string {
const path =
this.coreSetup.http.basePath.serverBasePath +
- (request.url.path || '/app/opensearch-dashboards');
+ (request.url.pathname || '/app/opensearch-dashboards');
return escape(path);
}
- private redirectToLoginUri(request: OpenSearchDashboardsRequest, toolkit: AuthToolkit) {
+ private redirectSAMlCapture = (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}`,
+ location: `${this.coreSetup.http.basePath.serverBasePath}/auth/saml/captureUrlFragment?nextUrl=${nextUrl}`,
'set-cookie': clearOldVersionCookie,
});
- }
+ };
private setupRoutes(): void {
const samlAuthRoutes = new SamlAuthRoutes(
@@ -112,7 +112,7 @@ export class SamlAuthentication extends AuthenticationType {
toolkit: AuthToolkit
): IOpenSearchDashboardsResponse | AuthResult {
if (this.isPageRequest(request)) {
- return this.redirectToLoginUri(request, toolkit);
+ return this.redirectSAMlCapture(request, toolkit);
} else {
return response.unauthorized();
}
diff --git a/server/session/security_cookie.ts b/server/session/security_cookie.ts
index 7cd172a90..50b880d9b 100644
--- a/server/session/security_cookie.ts
+++ b/server/session/security_cookie.ts
@@ -36,6 +36,7 @@ export interface SecuritySessionCookie {
saml?: {
requestId?: string;
nextUrl?: string;
+ redirectHash?: boolean;
};
}