diff --git a/docs/samples/browser-plugin-meetings/app.js b/docs/samples/browser-plugin-meetings/app.js index 637575c6434..7b5e81b578d 100644 --- a/docs/samples/browser-plugin-meetings/app.js +++ b/docs/samples/browser-plugin-meetings/app.js @@ -153,8 +153,23 @@ function initOauth() { webex.once('ready', () => { oauthFormElm.addEventListener('submit', (event) => { event.preventDefault(); + const isIframe = window !== window.parent; + + if (isIframe) { + webex.authorization.initiateLogin({ separateWindow: { + width: 800, + height: 600, + // You can also add other window features + menubar: 'no', + toolbar: 'no', + location: 'yes' + } }); + } else { + webex.authorization.initiateLogin(); + } + // initiate the login sequence if not authenticated. - webex.authorization.initiateLogin(); + }); if (webex.canAuthorize) { diff --git a/packages/@webex/plugin-authorization-browser-first-party/src/authorization.js b/packages/@webex/plugin-authorization-browser-first-party/src/authorization.js index 4c79d317fb8..3b09f372ed3 100644 --- a/packages/@webex/plugin-authorization-browser-first-party/src/authorization.js +++ b/packages/@webex/plugin-authorization-browser-first-party/src/authorization.js @@ -220,10 +220,34 @@ const Authorization = WebexPlugin.extend({ */ initiateAuthorizationCodeGrant(options) { this.logger.info('authorization: initiating authorization code grant flow'); - this.webex.getWindow().location = this.webex.credentials.buildLoginUrl( + const loginUrl = this.webex.credentials.buildLoginUrl( Object.assign({response_type: 'code'}, options) ); + if (options?.separateWindow) { + // Default window settings + const defaultWindowSettings = { + width: 600, + height: 800 + }; + + // Merge user provided settings with defaults + const windowSettings = Object.assign( + defaultWindowSettings, + typeof options.separateWindow === 'object' ? options.separateWindow : {} + ); + // Convert settings object to window.open features string + const windowFeatures = Object.entries(windowSettings) + .map(([key, value]) => `${key}=${value}`) + .join(','); + this.webex.getWindow().open(loginUrl, '_blank', windowFeatures); + } else { + // Default behavior - open in same window + this.webex.getWindow().location = loginUrl; + } + + + return Promise.resolve(); }, diff --git a/packages/@webex/plugin-authorization-browser-first-party/test/unit/spec/authorization.js b/packages/@webex/plugin-authorization-browser-first-party/test/unit/spec/authorization.js index a5a95285de2..210c4045c08 100644 --- a/packages/@webex/plugin-authorization-browser-first-party/test/unit/spec/authorization.js +++ b/packages/@webex/plugin-authorization-browser-first-party/test/unit/spec/authorization.js @@ -428,18 +428,77 @@ describe('plugin-authorization-browser-first-party', () => { describe('#initiateAuthorizationCodeGrant()', () => { it('redirects to the login page with response_type=code', () => { - const webex = makeWebex(undefined, undefined, { - credentials: { - clientType: 'confidential', - }, - }); + const webex = makeWebex(undefined, undefined, { + credentials: { + clientType: 'confidential', + }, + }); - sinon.spy(webex.authorization, 'initiateAuthorizationCodeGrant'); + sinon.spy(webex.authorization, 'initiateAuthorizationCodeGrant'); - return webex.authorization.initiateLogin().then(() => { - assert.called(webex.authorization.initiateAuthorizationCodeGrant); - assert.include(webex.getWindow().location, 'response_type=code'); - }); + return webex.authorization.initiateLogin().then(() => { + assert.called(webex.authorization.initiateAuthorizationCodeGrant); + assert.include(webex.getWindow().location, 'response_type=code'); + }); + }); + + it('redirects to the login page in the same window by default', () => { + const webex = makeWebex(); + + return webex.authorization.initiateAuthorizationCodeGrant().then(() => { + assert.isDefined(webex.getWindow().location); + assert.isUndefined(webex.getWindow().open); + }); + }); + + it('opens login page in a new window when separateWindow is true', () => { + const webex = makeWebex(); + webex.getWindow().open = sinon.spy(); + + return webex.authorization.initiateAuthorizationCodeGrant({ separateWindow: true }).then(() => { + assert.called(webex.getWindow().open); + const openCall = webex.getWindow().open.getCall(0); + assert.equal(openCall.args[1], '_blank'); + assert.equal(openCall.args[2], 'width=600,height=800'); + }); + }); + + it('opens login page in a new window with custom dimensions', () => { + const webex = makeWebex(); + webex.getWindow().open = sinon.spy(); + + const customWindow = { + width: 800, + height: 600, + menubar: 'no', + toolbar: 'no' + }; + + return webex.authorization.initiateAuthorizationCodeGrant({ + separateWindow: customWindow + }).then(() => { + assert.called(webex.getWindow().open); + const openCall = webex.getWindow().open.getCall(0); + assert.equal(openCall.args[1], '_blank'); + assert.equal( + openCall.args[2], + 'width=800,height=600,menubar=no,toolbar=no' + ); + }); + }); + + it('preserves other options when using separateWindow', () => { + const webex = makeWebex(); + webex.getWindow().open = sinon.spy(); + + return webex.authorization.initiateAuthorizationCodeGrant({ + separateWindow: true, + state: {} + }).then(() => { + assert.called(webex.getWindow().open); + const url = webex.getWindow().open.getCall(0).args[0]; + assert.include(url, "https://idbrokerbts.webex.com/idb/oauth2/v1/authorize?response_type=code&separateWindow=true&client_id=fake&redirect_uri=http%3A%2F%2Fexample.com&scope=scope%3Aone"); + }); }); }); diff --git a/packages/@webex/plugin-authorization-browser/src/authorization.js b/packages/@webex/plugin-authorization-browser/src/authorization.js index c2affd69fc5..76d82cd2d8a 100644 --- a/packages/@webex/plugin-authorization-browser/src/authorization.js +++ b/packages/@webex/plugin-authorization-browser/src/authorization.js @@ -113,13 +113,26 @@ const Authorization = WebexPlugin.extend({ return ret; }, - /** - * Kicks off an oauth flow - * @instance - * @memberof AuthorizationBrowser - * @param {Object} options - * @returns {Promise} - */ +/** + * Initiates the OAuth flow for user authentication. + * This function determines the type of OAuth flow to use based on the client type configuration. + * If the client is configured as "confidential", it will initiate the Authorization Code Grant flow; + * otherwise, it will initiate the Implicit Grant flow. + * + * @instance + * @memberof AuthorizationBrowser + * @param {Object} options - The options to configure the OAuth flow. + * @param {Object} [options.state] - An optional state object that can be used to include additional + * information such as security tokens. A CSRF token will be automatically generated and added to + * this state object. + * @param {boolean|Object} [options.separateWindow] - Determines if the login should open in a separate window. + * This can be a boolean or an object specifying window features: + * - If `true`, a new window with default dimensions is opened. + * - If an object, custom window features can be specified (e.g., `{width: 800, height: 600}`). + * @returns {Promise} - A promise that resolves when the appropriate OAuth flow has been initiated. + * The promise does not necessarily indicate the completion of the login process. + * @throws {Error} - Throws an error if there are issues initiating the OAuth flow. + */ initiateLogin(options = {}) { options.state = options.state || {}; options.state.csrf_token = this._generateSecurityToken(); @@ -134,20 +147,52 @@ const Authorization = WebexPlugin.extend({ }, @whileInFlight('isAuthorizing') - /** - * Kicks off the Authorization Code grant flow. Typically called via - * {@link AuthorizationBrowser#initiateLogin} - * @instance - * @memberof AuthorizationBrowser - * @param {Object} options - * @returns {Promise} - */ +/** + * Initiates the Implicit Grant flow for authorization. + * This function constructs the login URL and either opens it in a new + * window or in the current window based on the provided options. + * Typically called via {@link AuthorizationBrowser#initiateLogin}. + * + * @instance + * @memberof AuthorizationBrowser + * @param {Object} options - The options to configure the login flow. + * @param {Object} [options.separateWindow] - Determines if the login should open in a separate window. + * This can be a boolean or an object specifying window features: + * - If `true`, a new window with default dimensions is opened. + * - If an object, custom window features can be specified (e.g., `{width: 800, height: 600}`). + * @returns {Promise} - A promise that resolves immediately after initiating the login flow. + * This promise does not indicate the completion of the login process. + * @throws {Error} - Throws an error if the login URL cannot be constructed or if window opening fails. + */ initiateImplicitGrant(options) { + this.logger.info('authorization: initiating implicit grant flow'); - this.webex.getWindow().location = this.webex.credentials.buildLoginUrl( + const loginUrl = this.webex.credentials.buildLoginUrl( Object.assign({response_type: 'token'}, options) ); + if (options?.separateWindow) { + // Default window settings + const defaultWindowSettings = { + width: 600, + height: 800 + }; + + // Merge user provided settings with defaults + const windowSettings = Object.assign( + defaultWindowSettings, + typeof options.separateWindow === 'object' ? options.separateWindow : {} + ); + // Convert settings object to window.open features string + const windowFeatures = Object.entries(windowSettings) + .map(([key, value]) => `${key}=${value}`) + .join(','); + this.webex.getWindow().open(loginUrl, '_blank', windowFeatures); + } else { + // Default behavior - open in same window + this.webex.getWindow().location = loginUrl; + } + return Promise.resolve(); }, diff --git a/packages/@webex/plugin-authorization-browser/test/unit/spec/authorization.js b/packages/@webex/plugin-authorization-browser/test/unit/spec/authorization.js index bb9c3ab4c41..161cc26ca8b 100644 --- a/packages/@webex/plugin-authorization-browser/test/unit/spec/authorization.js +++ b/packages/@webex/plugin-authorization-browser/test/unit/spec/authorization.js @@ -408,6 +408,65 @@ describe('plugin-authorization-browser', () => { assert.include(webex.getWindow().location, 'response_type=token'); }); }); + + it('redirects to the login page in the same window by default', () => { + const webex = makeWebexCore(); + + return webex.authorization.initiateImplicitGrant().then(() => { + assert.isDefined(webex.getWindow().location); + assert.isUndefined(webex.getWindow().open); + }); + }); + + it('opens login page in a new window when separateWindow is true', () => { + const webex = makeWebexCore(); + webex.getWindow().open = sinon.spy(); + + return webex.authorization.initiateImplicitGrant({ separateWindow: true }).then(() => { + assert.called(webex.getWindow().open); + const openCall = webex.getWindow().open.getCall(0); + assert.equal(openCall.args[1], '_blank'); + assert.equal(openCall.args[2], 'width=600,height=800'); + }); + }); + + it('opens login page in a new window with custom dimensions', () => { + const webex = makeWebexCore(); + webex.getWindow().open = sinon.spy(); + + const customWindow = { + width: 800, + height: 600, + menubar: 'no', + toolbar: 'no' + }; + + return webex.authorization.initiateImplicitGrant({ + separateWindow: customWindow + }).then(() => { + assert.called(webex.getWindow().open); + const openCall = webex.getWindow().open.getCall(0); + assert.equal(openCall.args[1], '_blank'); + assert.equal( + openCall.args[2], + 'width=800,height=600,menubar=no,toolbar=no' + ); + }); + }); + + it('preserves other options when using separateWindow', () => { + const webex = makeWebexCore(); + webex.getWindow().open = sinon.spy(); + + return webex.authorization.initiateImplicitGrant({ + separateWindow: true, + state: {} + }).then(() => { + assert.called(webex.getWindow().open); + const url = webex.getWindow().open.getCall(0).args[0]; + assert.include(url, "https://idbrokerbts.webex.com/idb/oauth2/v1/authorize?response_type=token&separateWindow=true&client_id=fake&redirect_uri=http%3A%2F%2Fexample.com&scope=scope%3Aone"); + }); + }); }); describe('#initiateAuthorizationCodeGrant()', () => {