diff --git a/examples/login_demo.html b/examples/login_demo.html index fe657f6..3dc876a 100644 --- a/examples/login_demo.html +++ b/examples/login_demo.html @@ -242,11 +242,11 @@

SDK - SSO Login Demo

- +
- +
@@ -296,7 +296,7 @@

SDK - SSO Login Demo

const sdbClient = new SlashDBClient(sdbConfig); let status = await sdbClient.login(); - +
diff --git a/examples/popup_success.html b/examples/popup_success.html new file mode 100644 index 0000000..85fe791 --- /dev/null +++ b/examples/popup_success.html @@ -0,0 +1,13 @@ + + + SlashDB JavaScript SDK Demo + + + +
+

Login was successful, you can close this window...

+
+ + + + \ No newline at end of file diff --git a/examples/success.html b/examples/success.html index 54c195f..7df7b63 100644 --- a/examples/success.html +++ b/examples/success.html @@ -1,13 +1,145 @@ SlashDB JavaScript SDK Demo + +

Login was successful, you can close this window...

-
+
+ +
+ SlashDB Single Sign-On redirect page
+ Requires a proper build of redirect parameters +
        const sdbConfig = {
+            host: 'http://localhost:6543',
+            sso: {
+                idpId: 'keycloak',
+                redirectUri: 'http://localhost:8081/examples/success.html',
+                popUp: true
+            }
+        }
+        const sdbClient = new SlashDBClient(sdbConfig);
+        const success = await sdbClient.buildSSORedirect();
+        const isAuthenticated = await sdbClient.isAuthenticated();
+        
+ +
diff --git a/src/baserequesthandler.js b/src/baserequesthandler.js index 0612ca1..a6c2ff4 100644 --- a/src/baserequesthandler.js +++ b/src/baserequesthandler.js @@ -134,8 +134,8 @@ class BaseRequestHandler { 'Content-Type': this.contentTypeHeader, ...this.extraHeaders }; - } else if (this.sdbClient.ssoCredentials) { - const token = btoa(this.sdbClient.ssoCredentials.id_token) + } else if (this.sdbClient.jwtCredentials) { + const token = btoa(this.sdbClient.jwtCredentials.id_token) headers = { Authorization: "Bearer " + token, "X-Identity-Provider-Id": this.sdbClient.sso.idpId, diff --git a/src/slashdbclient.js b/src/slashdbclient.js index f59d886..75899c8 100644 --- a/src/slashdbclient.js +++ b/src/slashdbclient.js @@ -2,7 +2,7 @@ import { DataDiscoveryDatabase } from './datadiscovery.js' import { SQLPassThruQuery } from './sqlpassthru.js' import { BaseRequestHandler } from './baserequesthandler.js'; import { PKCE, generateCodeVerifier, generateCodeChallenge } from './pkce.js'; -import { getUrlParms, isSSOredirect, popupCenter } from "./utils.js"; +import { getUrlParms, isJWTredirect, popupCenter } from "./utils.js"; const SDB_SDBC_INVALID_HOSTNAME = 'Invalid hostname parameter, must be string'; const SDB_SDBC_INVALID_USERNAME = 'Invalid username parameter, must be string'; @@ -79,7 +79,7 @@ class SlashDBClient { this.sso.popUp = popUp; } - this.ssoCredentials = null; + this.jwtCredentials = null; // create the special case BaseRequestHandler object for interacting with config endpoints this.sdbConfig = new BaseRequestHandler(this); @@ -139,7 +139,7 @@ class SlashDBClient { } } else if (sso.idpId && sso.redirectUri) { await this.loginSSO(sso.popUp).then((resp) => { - this.ssoCredentials = resp; + this.jwtCredentials = resp; }); let settings = (await this.sdbConfig.get(this.settingsEP)).data; this.username = settings.user; @@ -172,16 +172,16 @@ class SlashDBClient { async buildSSORedirect(){ const urlParams = getUrlParms(); - if (isSSOredirect(urlParams)){ - const ssoConfig = await this._getSsoConfig(); + if (isJWTredirect(urlParams)){ + const jwtConfig = await this._getJWTConfig(); const url = window.location.href; - const pkce = new PKCE(ssoConfig); + const pkce = new PKCE(jwtConfig); this.sso.idpId = sessionStorage.getItem('ssoApp.idp_id'); pkce.codeVerifier = sessionStorage.getItem('ssoApp.code_verifier'); return new Promise((resolve, reject) => { pkce.exchangeForAccessToken(url).then((resp) => { - this.ssoCredentials = resp; + this.jwtCredentials = resp; resolve(true); }); }); @@ -194,16 +194,44 @@ class SlashDBClient { */ async loginSSO(popUp) { + const settings = await this.getSettings(); + const jwtSettings = settings.auth_settings.authentication_policies.jwt; + const samlSettings = settings.auth_settings.authentication_policies.saml; + + const idpId = this.sso.idpId; + + if (!jwtSettings.identity_providers.hasOwnProperty(idpId) && !samlSettings.identity_providers.hasOwnProperty(idpId)) { + throw new Error(SDB_SDBC_IDENTITY_PROVIDER_NOT_AVAILABLE); + } + popUp = popUp ? popUp : this.sso.popUp; - const ssoConfig = await this._getSsoConfig(); - const pkce = new PKCE(ssoConfig); + if (jwtSettings.identity_providers.hasOwnProperty(idpId)) { + return this.loginJWT(popUp); + } + + if (samlSettings.identity_providers.hasOwnProperty(idpId)) { + return this.loginSAML(popUp); + } + + } + + /** + * Logs in to SlashDB instance with JWT token. Only required when using SSO. + * @param {boolean} popUp - optional flag to sign in against the identity provider with a Pop Up window (false by default) + */ + + async loginJWT(popUp) { + + const jwtConfig = await this._getJWTConfig(); + const pkce = new PKCE(jwtConfig); const additionalParams = await this._buildSession(); let loginUrl = await pkce.authorizeUrl(additionalParams); if (!popUp) { window.location.replace(loginUrl); + return; } const width = 500; @@ -213,7 +241,7 @@ class SlashDBClient { return new Promise((resolve, reject) => { const checkPopup = setInterval(() => { - const pkce = new PKCE(ssoConfig); + const pkce = new PKCE(jwtConfig); let popUpHref = ""; try { popUpHref = popupWindow.window.location.href; @@ -235,18 +263,60 @@ class SlashDBClient { }); } + /** + * Logs in to SlashDB instance with SAML token. Only required when using SSO. + * @param {boolean} popUp - optional flag to sign in against the identity provider with a Pop Up window (false by default) + */ + + async loginSAML(popUp){ + + const sso = this.sso; + const ref = sso.redirectUri; + const idpId = sso.idpId; + + let path = `/sso/saml/${idpId}?return_to=${ref}`; + let loginUrl = this.sdbConfig._buildEndpointString(path); + + if (!popUp) { + window.location.replace(loginUrl); + return; + } + + const width = 500; + const height = 600; + + const popupWindow = popupCenter(loginUrl, "login", width, height); + + return new Promise((resolve, reject) => { + const checkPopup = setInterval(() => { + let popUpHref = ""; + try { + popUpHref = popupWindow.window.location.href; + } catch (e) { + console.warn(e); + } + if (popUpHref.startsWith(window.location.origin)) { + popupWindow.close(); + } + if (!popupWindow || !popupWindow.closed) return; + clearInterval(checkPopup); + resolve(true); + }, 250); + }); + } + /** * Refreshes the SSO access token. */ async refreshSSOToken(){ - const ssoConfig = await this._getSsoConfig(); - const pkce = new PKCE(ssoConfig); - const refreshToken = this.ssoCredentials.refresh_token; + const jwtConfig = await this._getJWTConfig(); + const pkce = new PKCE(jwtConfig); + const refreshToken = this.jwtCredentials.refresh_token; return new Promise((resolve, reject) => { pkce.refreshAccessToken(refreshToken).then((resp) => { - this.ssoCredentials = resp; + this.jwtCredentials = resp; resolve(true); }); }); @@ -258,7 +328,10 @@ class SlashDBClient { * @returns {boolean} boolean - to indicate if currently authenticated */ async isAuthenticated() { - const url = `${this.userEP}/${this.username}.json`; + + const settings = await this.getSettings(); + const username = settings.user; + const url = `${this.userEP}/${username}.json`; try { let response = (await this.sdbConfig.get(url)).res @@ -280,7 +353,7 @@ class SlashDBClient { async logout() { try { await this.sdbConfig.get(this.logoutEP); - this.ssoCredentials = null; + this.jwtCredentials = null; this._clearSession(); } catch(e) { @@ -440,12 +513,12 @@ class SlashDBClient { return queries; } - async _getSsoConfig() { + async _getJWTConfig() { let response = (await this.sdbConfig.get(this.settingsEP)).data; let idpId = this.sso.idpId; let redirectUri = this.sso.redirectUri; - const jwtSettings = response.auth_settings.authentication_policies.jwt + const jwtSettings = response.auth_settings.authentication_policies.jwt; if (!jwtSettings.identity_providers.hasOwnProperty(this.sso.idpId)) { throw new Error(SDB_SDBC_IDENTITY_PROVIDER_NOT_AVAILABLE); @@ -462,7 +535,7 @@ class SlashDBClient { redirectUri = idpSettings.redirect_uri; } - const ssoConfig = { + const jwtConfig = { idp_id: idpId, client_id: clientId, redirect_uri: redirectUri, @@ -471,7 +544,7 @@ class SlashDBClient { requested_scopes: requestedScopes, } - return ssoConfig; + return jwtConfig; } async _buildSession() { diff --git a/src/utils.js b/src/utils.js index 95d000b..36eea27 100644 --- a/src/utils.js +++ b/src/utils.js @@ -42,15 +42,15 @@ function isObjectEmpty(objectName) { return Object.keys(objectName).length === 0 } -function isSSOredirect(ssoParams) { +function isJWTredirect(jwtParams) { - if (isObjectEmpty(ssoParams)){ + if (isObjectEmpty(jwtParams)){ return false; } - const state = ssoParams.state; - const sessionState = ssoParams.session_state; - const code = ssoParams.code; + const state = jwtParams.state; + const sessionState = jwtParams.session_state; + const code = jwtParams.code; if (!state || typeof(state) !== 'string') { return false @@ -68,4 +68,4 @@ function isSSOredirect(ssoParams) { } -export { getUrlParms, popupCenter, isSSOredirect } \ No newline at end of file +export { getUrlParms, popupCenter, isJWTredirect } \ No newline at end of file