Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement nextUrl for OpenID Authentication #1563

Merged
merged 13 commits into from
Dec 8, 2023
1 change: 0 additions & 1 deletion .github/workflows/cypress-test-oidc-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ jobs:
else
echo "Checksum match confirmed. Proceeding with setup."
fi
chmod +x ./kc.sh
cwperks marked this conversation as resolved.
Show resolved Hide resolved

# Setup and Run Keycloak
- name: Get and run Keycloak on Linux
Expand Down
3 changes: 2 additions & 1 deletion common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ export const CUSTOM_ERROR_PAGE_URI = '/app/' + APP_ID_CUSTOMERROR;
export const API_AUTH_LOGIN = '/auth/login';
export const API_AUTH_LOGOUT = '/auth/logout';
export const OPENID_AUTH_LOGIN = '/auth/openid/login';
export const OPENID_AUTH_LOGIN_WITH_FRAGMENT = '/auth/openid/captureUrlFragment';
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';
export const ANONYMOUS_AUTH_LOGIN = '/auth/anonymous';

export const OPENID_AUTH_LOGOUT = '/auth/openid/logout';
export const SAML_AUTH_LOGOUT = '/auth/saml/logout';
Expand Down
6 changes: 4 additions & 2 deletions public/apps/login/login-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import { validateCurrentPassword } from '../../utils/login-utils';
import {
ANONYMOUS_AUTH_LOGIN,
AuthType,
OPENID_AUTH_LOGIN,
OPENID_AUTH_LOGIN_WITH_FRAGMENT,
SAML_AUTH_LOGIN_WITH_FRAGMENT,
} from '../../../common';

Expand Down Expand Up @@ -228,7 +228,9 @@ export function LoginPage(props: LoginPageDeps) {
}
case AuthType.OPEN_ID: {
const oidcConfig = props.config.ui[AuthType.OPEN_ID].login;
formBodyOp.push(renderLoginButton(AuthType.OPEN_ID, OPENID_AUTH_LOGIN, oidcConfig));
const nextUrl = extractNextUrlFromWindowLocation();
const oidcAuthLoginUrl = OPENID_AUTH_LOGIN_WITH_FRAGMENT + nextUrl;
formBodyOp.push(renderLoginButton(AuthType.OPEN_ID, oidcAuthLoginUrl, oidcConfig));
break;
}
case AuthType.SAML: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ exports[`Login page renders renders with config value for multiauth 1`] = `
aria-label="openid_login_button"
className="test-btn-style"
data-test-subj="submit"
href="/app/opensearch-dashboards/auth/openid/login"
href="/app/opensearch-dashboards/auth/openid/captureUrlFragment?nextUrl=%2F"
cwperks marked this conversation as resolved.
Show resolved Hide resolved
iconType="http://localhost:5601/images/test.png"
size="s"
type="prime"
Expand Down
35 changes: 23 additions & 12 deletions server/auth/types/openid/openid_auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,17 @@ import {
LifecycleResponseFactory,
AuthToolkit,
IOpenSearchDashboardsResponse,
AuthResult,
} from 'opensearch-dashboards/server';
import HTTP from 'http';
import HTTPS from 'https';
import { PeerCertificate } from 'tls';
import { Server, ServerStateCookieOptions } from '@hapi/hapi';
import { SecurityPluginConfigType } from '../../..';
import { SecuritySessionCookie } from '../../../session/security_cookie';
import {
SecuritySessionCookie,
clearOldVersionCookieValue,
} from '../../../session/security_cookie';
import { OpenIdAuthRoutes } from './routes';
import { AuthenticationType } from '../authentication_type';
import { callTokenEndpoint } from './helper';
Expand Down Expand Up @@ -124,6 +128,22 @@ export class OpenIdAuthentication extends AuthenticationType {
}
}

private generateNextUrl(request: OpenSearchDashboardsRequest): string {
const path =
this.coreSetup.http.basePath.serverBasePath +
(request.url.pathname || '/app/opensearch-dashboards');
return escape(path);
}

private redirectOIDCCapture = (request: OpenSearchDashboardsRequest, toolkit: AuthToolkit) => {
const nextUrl = this.generateNextUrl(request);
const clearOldVersionCookie = clearOldVersionCookieValue(this.config);
cwperks marked this conversation as resolved.
Show resolved Hide resolved
return toolkit.redirected({
location: `${this.coreSetup.http.basePath.serverBasePath}/auth/openid/captureUrlFragment?nextUrl=${nextUrl}`,
'set-cookie': clearOldVersionCookie,
});
};

private createWreckClient(): typeof wreck {
if (this.config.openid?.root_ca) {
this.wreckHttpsOption.ca = [fs.readFileSync(this.config.openid.root_ca)];
Expand Down Expand Up @@ -297,18 +317,9 @@ export class OpenIdAuthentication extends AuthenticationType {
request: OpenSearchDashboardsRequest,
response: LifecycleResponseFactory,
toolkit: AuthToolkit
): IOpenSearchDashboardsResponse {
): IOpenSearchDashboardsResponse | AuthResult {
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}${OPENID_AUTH_LOGIN}?${nextUrl}`,
},
});
return this.redirectOIDCCapture(request, toolkit);
} else {
return response.unauthorized();
}
Expand Down
134 changes: 129 additions & 5 deletions server/auth/types/openid/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export class OpenIdAuthRoutes {
validate: validateNextUrl,
})
),
redirectHash: schema.maybe(schema.string()),
cwperks marked this conversation as resolved.
Show resolved Hide resolved
state: schema.maybe(schema.string()),
refresh: schema.maybe(schema.string()),
},
Expand Down Expand Up @@ -135,6 +136,7 @@ export class OpenIdAuthRoutes {
oidc: {
state: nonce,
nextUrl: getNextUrl(this.config, this.core, request),
redirectHash: request.query.redirectHash === 'true',
},
authType: AuthType.OPEN_ID,
};
Expand Down Expand Up @@ -164,6 +166,7 @@ export class OpenIdAuthRoutes {
const nextUrl: string = cookie.oidc.nextUrl;
const clientId = this.config.openid?.client_id;
const clientSecret = this.config.openid?.client_secret;
const redirectHash: boolean = cookie.oidc?.redirectHash || false;
cwperks marked this conversation as resolved.
Show resolved Hide resolved
const query: any = {
grant_type: AUTH_GRANT_TYPE,
code: request.query.code,
Expand Down Expand Up @@ -211,11 +214,21 @@ export class OpenIdAuthRoutes {
);

this.sessionStorageFactory.asScoped(request).set(sessionStorage);
return response.redirected({
headers: {
location: nextUrl,
},
});
if (redirectHash) {
return response.redirected({
headers: {
location: `${
this.core.http.basePath.serverBasePath
}/auth/openid/redirectUrlFragment?nextUrl=${escape(nextUrl)}`,
},
});
} else {
return response.redirected({
headers: {
location: nextUrl,
},
});
}
} catch (error: any) {
context.security_plugin.logger.error(`OpenId authentication failed: ${error}`);
if (error.toString().toLowerCase().includes('authentication exception')) {
Expand Down Expand Up @@ -271,5 +284,116 @@ export class OpenIdAuthRoutes {
});
}
);

// captureUrlFragment is the first route that will be invoked in the SP initiated login.
// This route will execute the captureUrlFragment.js script.
this.core.http.resources.register(
{
path: '/auth/openid/captureUrlFragment',
cwperks marked this conversation as resolved.
Show resolved Hide resolved
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.core.http.basePath.serverBasePath;
return response.renderHtml({
body: `
<!DOCTYPE html>
<title>OSD OIDC Capture</title>
<link rel="icon" href="data:,">
<script src="${serverBasePath}/auth/openid/captureUrlFragment.js"></script>
`,
});
}
);

// This script will store the URL Hash in browser's local storage.
this.core.http.resources.register(
{
path: '/auth/openid/captureUrlFragment.js',
validate: false,
options: {
authRequired: false,
},
},
async (context, request, response) => {
this.sessionStorageFactory.asScoped(request).clear();
return response.renderJs({
body: `let oidcHash=window.location.hash.toString();
let redirectHash = false;
if (oidcHash !== "") {
window.localStorage.removeItem('oidcHash');
window.localStorage.setItem('oidcHash', oidcHash);
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 the browser will be redirected to '/auth/openid/redirectUrlFragment'
// route, which will execute the redirectUrlFragment.js.
this.core.http.resources.register(
{
path: '/auth/openid/redirectUrlFragment',
validate: {
query: schema.object({
nextUrl: schema.any(),
}),
},
options: {
authRequired: true,
},
},
async (context, request, response) => {
const serverBasePath = this.core.http.basePath.serverBasePath;
return response.renderHtml({
body: `
<!DOCTYPE html>
<title>OSD OpenID Success</title>
<link rel="icon" href="data:,">
<script src="${serverBasePath}/auth/openid/redirectUrlFragment.js"></script>
`,
});
}
);

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