Skip to content

Commit

Permalink
Implement nextUrl for OpenID Authentication (opensearch-project#1563)
Browse files Browse the repository at this point in the history
Signed-off-by: Craig Perkins <[email protected]>
Co-authored-by: Darshit Chanpura <[email protected]>
  • Loading branch information
cwperks and DarshitChanpura authored Dec 8, 2023
1 parent 6dcd7d5 commit 6374198
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 71 deletions.
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 @@ -48,7 +48,6 @@ jobs:
echo "Unpacking Keycloak"
tar -xzf keycloak-${{ env.KEYCLOAK_VERSION }}.tar.gz
cd keycloak-${{ env.KEYCLOAK_VERSION }}/bin
chmod +x ./kc.sh
echo "Generating checksum for the downloaded kc.sh script..."
DOWNLOADED_CHECKSUM=$(sha256sum kc.sh | awk '{print $1}')
echo "Downloaded kc.sh checksum: $DOWNLOADED_CHECKSUM"
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"
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);
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.boolean()),
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;
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',
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);
`,
});
}
);
}
}
6 changes: 5 additions & 1 deletion server/session/security_cookie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ export interface SecuritySessionCookie {
tenant?: any;

// for oidc auth workflow
oidc?: any;
oidc?: {
state?: string;
nextUrl?: string;
redirectHash?: boolean;
};

// for Saml auth workflow
saml?: {
Expand Down
Loading

0 comments on commit 6374198

Please sign in to comment.