Skip to content

Commit f4c89ee

Browse files
Preserve URL Hash for SAML based login (#1039) (#1054)
* Preserve URL HASH after user logs via SAML IDP Co-authored-by: Darshit Chanpura <[email protected]> (cherry picked from commit a9d10d8) Co-authored-by: Deepak Devarakonda <[email protected]>
1 parent 5ccf7c9 commit f4c89ee

File tree

3 files changed

+138
-10
lines changed

3 files changed

+138
-10
lines changed

server/auth/types/saml/routes.ts

Lines changed: 132 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export class SamlAuthRoutes {
4646
validate: validateNextUrl,
4747
})
4848
),
49+
redirectHash: schema.string(),
4950
}),
5051
},
5152
options: {
@@ -67,6 +68,7 @@ export class SamlAuthRoutes {
6768
saml: {
6869
nextUrl: request.query.nextUrl,
6970
requestId: samlHeader.requestId,
71+
redirectHash: request.query.redirectHash === 'true',
7072
},
7173
};
7274
this.sessionStorageFactory.asScoped(request).set(cookie);
@@ -95,13 +97,15 @@ export class SamlAuthRoutes {
9597
async (context, request, response) => {
9698
let requestId: string = '';
9799
let nextUrl: string = '/';
100+
let redirectHash: boolean = false;
98101
try {
99102
const cookie = await this.sessionStorageFactory.asScoped(request).get();
100103
if (cookie) {
101104
requestId = cookie.saml?.requestId || '';
102105
nextUrl =
103106
cookie.saml?.nextUrl ||
104107
`${this.coreSetup.http.basePath.serverBasePath}/app/opensearch-dashboards`;
108+
redirectHash = cookie.saml?.redirectHash || false;
105109
}
106110
if (!requestId) {
107111
return response.badRequest({
@@ -143,11 +147,21 @@ export class SamlAuthRoutes {
143147
expiryTime,
144148
};
145149
this.sessionStorageFactory.asScoped(request).set(cookie);
146-
return response.redirected({
147-
headers: {
148-
location: nextUrl,
149-
},
150-
});
150+
if (redirectHash) {
151+
return response.redirected({
152+
headers: {
153+
location: `${
154+
this.coreSetup.http.basePath.serverBasePath
155+
}/auth/saml/redirectUrlFragment?nextUrl=${escape(nextUrl)}`,
156+
},
157+
});
158+
} else {
159+
return response.redirected({
160+
headers: {
161+
location: nextUrl,
162+
},
163+
});
164+
}
151165
} catch (error) {
152166
context.security_plugin.logger.error(
153167
`SAML SP initiated authentication workflow failed: ${error}`
@@ -215,6 +229,119 @@ export class SamlAuthRoutes {
215229
}
216230
);
217231

232+
// captureUrlFragment is the first route that will be invoked in the SP initiated login.
233+
// This route will execute the captureUrlFragment.js script.
234+
this.coreSetup.http.resources.register(
235+
{
236+
path: '/auth/saml/captureUrlFragment',
237+
validate: {
238+
query: schema.object({
239+
nextUrl: schema.maybe(
240+
schema.string({
241+
validate: validateNextUrl,
242+
})
243+
),
244+
}),
245+
},
246+
options: {
247+
authRequired: false,
248+
},
249+
},
250+
async (context, request, response) => {
251+
this.sessionStorageFactory.asScoped(request).clear();
252+
const serverBasePath = this.coreSetup.http.basePath.serverBasePath;
253+
return response.renderHtml({
254+
body: `
255+
<!DOCTYPE html>
256+
<title>OSD SAML Capture</title>
257+
<link rel="icon" href="data:,">
258+
<script src="${serverBasePath}/auth/saml/captureUrlFragment.js"></script>
259+
`,
260+
});
261+
}
262+
);
263+
264+
// This script will store the URL Hash in browser's local storage.
265+
this.coreSetup.http.resources.register(
266+
{
267+
path: '/auth/saml/captureUrlFragment.js',
268+
validate: false,
269+
options: {
270+
authRequired: false,
271+
},
272+
},
273+
async (context, request, response) => {
274+
this.sessionStorageFactory.asScoped(request).clear();
275+
return response.renderJs({
276+
body: `let samlHash=window.location.hash.toString();
277+
let redirectHash = false;
278+
if (samlHash !== "") {
279+
window.localStorage.removeItem('samlHash');
280+
window.localStorage.setItem('samlHash', samlHash);
281+
redirectHash = true;
282+
}
283+
let params = new URLSearchParams(window.location.search);
284+
let nextUrl = params.get("nextUrl");
285+
finalUrl = "login?nextUrl=" + encodeURIComponent(nextUrl);
286+
finalUrl += "&redirectHash=" + encodeURIComponent(redirectHash);
287+
window.location.replace(finalUrl);
288+
289+
`,
290+
});
291+
}
292+
);
293+
294+
// Once the User is authenticated via the '_opendistro/_security/saml/acs' route,
295+
// the browser will be redirected to '/auth/saml/redirectUrlFragment' route,
296+
// which will execute the redirectUrlFragment.js.
297+
this.coreSetup.http.resources.register(
298+
{
299+
path: '/auth/saml/redirectUrlFragment',
300+
validate: {
301+
query: schema.object({
302+
nextUrl: schema.any(),
303+
}),
304+
},
305+
options: {
306+
authRequired: true,
307+
},
308+
},
309+
async (context, request, response) => {
310+
const serverBasePath = this.coreSetup.http.basePath.serverBasePath;
311+
return response.renderHtml({
312+
body: `
313+
<!DOCTYPE html>
314+
<title>OSD SAML Success</title>
315+
<link rel="icon" href="data:,">
316+
<script src="${serverBasePath}/auth/saml/redirectUrlFragment.js"></script>
317+
`,
318+
});
319+
}
320+
);
321+
322+
// This script will pop the Hash from local storage if it exists.
323+
// And forward the browser to the next url.
324+
this.coreSetup.http.resources.register(
325+
{
326+
path: '/auth/saml/redirectUrlFragment.js',
327+
validate: false,
328+
options: {
329+
authRequired: true,
330+
},
331+
},
332+
async (context, request, response) => {
333+
return response.renderJs({
334+
body: `let samlHash=window.localStorage.getItem('samlHash');
335+
window.localStorage.removeItem('samlHash');
336+
let params = new URLSearchParams(window.location.search);
337+
let nextUrl = params.get("nextUrl");
338+
finalUrl = nextUrl + samlHash;
339+
window.location.replace(finalUrl);
340+
`,
341+
});
342+
}
343+
);
344+
218345
this.router.get(
219346
{
220347
path: `/auth/logout`,

server/auth/types/saml/saml_auth.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,18 +54,18 @@ export class SamlAuthentication extends AuthenticationType {
5454
private generateNextUrl(request: OpenSearchDashboardsRequest): string {
5555
const path =
5656
this.coreSetup.http.basePath.serverBasePath +
57-
(request.url.path || '/app/opensearch-dashboards');
57+
(request.url.pathname || '/app/opensearch-dashboards');
5858
return escape(path);
5959
}
6060

61-
private redirectToLoginUri(request: OpenSearchDashboardsRequest, toolkit: AuthToolkit) {
61+
private redirectSAMlCapture = (request: OpenSearchDashboardsRequest, toolkit: AuthToolkit) => {
6262
const nextUrl = this.generateNextUrl(request);
6363
const clearOldVersionCookie = clearOldVersionCookieValue(this.config);
6464
return toolkit.redirected({
65-
location: `${this.coreSetup.http.basePath.serverBasePath}/auth/saml/login?nextUrl=${nextUrl}`,
65+
location: `${this.coreSetup.http.basePath.serverBasePath}/auth/saml/captureUrlFragment?nextUrl=${nextUrl}`,
6666
'set-cookie': clearOldVersionCookie,
6767
});
68-
}
68+
};
6969

7070
private setupRoutes(): void {
7171
const samlAuthRoutes = new SamlAuthRoutes(
@@ -112,7 +112,7 @@ export class SamlAuthentication extends AuthenticationType {
112112
toolkit: AuthToolkit
113113
): IOpenSearchDashboardsResponse | AuthResult {
114114
if (this.isPageRequest(request)) {
115-
return this.redirectToLoginUri(request, toolkit);
115+
return this.redirectSAMlCapture(request, toolkit);
116116
} else {
117117
return response.unauthorized();
118118
}

server/session/security_cookie.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export interface SecuritySessionCookie {
3636
saml?: {
3737
requestId?: string;
3838
nextUrl?: string;
39+
redirectHash?: boolean;
3940
};
4041
}
4142

0 commit comments

Comments
 (0)