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; }; }