From cb538d0426f0c76078da8eb442e7c024f3ff0c08 Mon Sep 17 00:00:00 2001 From: Jared Perreault <jared.perreault@okta.com> Date: Fri, 17 Jan 2025 18:52:58 -0500 Subject: [PATCH 1/2] feat: adds advancedOption to getWithPopup --- lib/oidc/getToken.ts | 23 ++++++++---- lib/oidc/getWithPopup.ts | 9 +++-- lib/oidc/types/api.ts | 5 ++- test/spec/oidc/getWithPopup.ts | 64 ++++++++++++++++++++++++++++++++++ yarn.lock | 31 ++-------------- 5 files changed, 95 insertions(+), 37 deletions(-) diff --git a/lib/oidc/getToken.ts b/lib/oidc/getToken.ts index 5c6b05ff1..d56199682 100644 --- a/lib/oidc/getToken.ts +++ b/lib/oidc/getToken.ts @@ -166,14 +166,25 @@ export function getToken(sdk: OktaAuthOAuthInterface, options: TokenParams & Pop popupWindow.location.assign(requestUrl); } - // The popup may be closed without receiving an OAuth response. Setup a poller to monitor the window. var popupPromise = new Promise(function (resolve, reject) { - var closePoller = setInterval(function () { - if (!popupWindow || popupWindow.closed) { - clearInterval(closePoller); - reject(new AuthSdkError('Unable to parse OAuth flow response')); + let closePoller; + // Using external IDPs within a popup can cause `popupWindow.closed` to prematurely return `true` + // the origin of the popup changes (to the external IDP). Disable the popup monitor to avoid this + // WARNING: This resulting promise will remain pending until the user completes the flow or timeout + if (options?.monitorPopupWindow === false) { + if (!options.timeout) { + throw new AuthSdkError('`timeout` must be set when `monitorPopupWindow` is set to `false`!'); } - }, 100); + } + else { + // The popup may be closed without receiving an OAuth response. Setup a poller to monitor the window. + closePoller = setInterval(function () { + if (!popupWindow || popupWindow.closed) { + clearInterval(closePoller); + reject(new AuthSdkError('Unable to parse OAuth flow response')); + } + }, 100); + } // Proxy the OAuth promise results oauthPromise diff --git a/lib/oidc/getWithPopup.ts b/lib/oidc/getWithPopup.ts index b4c153576..99cb89445 100644 --- a/lib/oidc/getWithPopup.ts +++ b/lib/oidc/getWithPopup.ts @@ -11,12 +11,17 @@ * */ import { AuthSdkError } from '../errors'; -import { OktaAuthOAuthInterface, TokenParams, TokenResponse } from './types'; +import { OktaAuthOAuthInterface, WithPopupOptions, TokenResponse } from './types'; import { clone } from '../util'; import { getToken } from './getToken'; import { loadPopup } from './util'; -export function getWithPopup(sdk: OktaAuthOAuthInterface, options: TokenParams): Promise<TokenResponse> { + +// `monitorPopupWindow: false` is an advance setting. Use with caution! +// When `false`, polling to determine whether or not the popup is open is perforemd. +// A `timeout` must be provided when `monitorPopupWindow` is set to `false` + +export function getWithPopup(sdk: OktaAuthOAuthInterface, options: WithPopupOptions): Promise<TokenResponse> { if (arguments.length > 2) { return Promise.reject(new AuthSdkError('As of version 3.0, "getWithPopup" takes only a single set of options')); } diff --git a/lib/oidc/types/api.ts b/lib/oidc/types/api.ts index 1299cd9f8..cde4e633f 100644 --- a/lib/oidc/types/api.ts +++ b/lib/oidc/types/api.ts @@ -25,6 +25,7 @@ import { Endpoints } from './endpoints'; export interface PopupParams { popupTitle?: string; popupWindow?: Window; + monitorPopupWindow?: boolean; } export interface TokenResponse { @@ -57,6 +58,8 @@ export interface BaseTokenAPI { exchangeCodeForTokens(params: TokenParams, urls?: CustomUrls): Promise<TokenResponse>; } +export type WithPopupOptions = TokenParams & { monitorPopupWindow?: boolean; }; + export interface TokenAPI extends BaseTokenAPI { getUserInfo<S extends CustomUserClaims = CustomUserClaims>( accessToken?: AccessToken, @@ -65,7 +68,7 @@ export interface TokenAPI extends BaseTokenAPI { getWithRedirect: GetWithRedirectFunction; parseFromUrl: ParseFromUrlInterface; getWithoutPrompt(params?: TokenParams): Promise<TokenResponse>; - getWithPopup(params?: TokenParams): Promise<TokenResponse>; + getWithPopup(params?: WithPopupOptions): Promise<TokenResponse>; revoke(token: RevocableToken): Promise<object>; renew(token: Token): Promise<Token | undefined>; renewTokens(options?: RenewTokensParams): Promise<Tokens>; diff --git a/test/spec/oidc/getWithPopup.ts b/test/spec/oidc/getWithPopup.ts index 0aa7de99a..da812e8f6 100644 --- a/test/spec/oidc/getWithPopup.ts +++ b/test/spec/oidc/getWithPopup.ts @@ -24,6 +24,9 @@ import tokens from '@okta/test.support/tokens'; import util from '@okta/test.support/util'; import oauthUtil from '@okta/test.support/oauthUtil'; import waitFor from '@okta/test.support/waitFor'; +import { setImmediate } from 'timers'; +import { AuthSdkError } from 'lib/errors'; + describe('token.getWithPopup', function() { beforeEach(() => { @@ -499,4 +502,65 @@ describe('token.getWithPopup', function() { }); }); + it('does not detect when popup window closes', async function () { + // `oauthUtil.setupPopup` performs 2 assertions (other 3 are locally in this test) + expect.assertions(5); // ensures the expect()s in .catch are called + const intervalSpy = jest.spyOn(global, 'setInterval'); + + jest.useFakeTimers(); + const promise = oauthUtil.setupPopup({ + willFail: true, + closePopup: true, + oktaAuthArgs: { + pkce: false, + issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7', + clientId: 'NPSfOkH5eZrTy8PMDlvx', + redirectUri: 'https://example.com/redirect' + }, + getWithPopupArgs: { + monitorPopupWindow: false, + timeout: 120000 + }, + }) + .catch(err => { + expect(err).toBeInstanceOf(AuthSdkError); + expect(err.message).toBe('OAuth flow timed out'); + }); + + // flushes promise queue (can be replaced by `jest.advanceTimersByTimeAsync` in jest@29) + await (new Promise(resolve => setImmediate(resolve))); + jest.advanceTimersByTime(150000); + await promise; + + expect(intervalSpy).not.toHaveBeenCalled(); + + jest.useRealTimers(); + }); + + it('throws if timeout is not provided', async function () { + // `oauthUtil.setupPopup` performs 2 assertions (other 3 are locally in this test) + expect.assertions(5); // ensures the expect()s in .catch are called + const intervalSpy = jest.spyOn(global, 'setInterval'); + + await oauthUtil.setupPopup({ + willFail: true, + closePopup: true, + oktaAuthArgs: { + pkce: false, + issuer: 'https://auth-js-test.okta.com/oauth2/aus8aus76q8iphupD0h7', + clientId: 'NPSfOkH5eZrTy8PMDlvx', + redirectUri: 'https://example.com/redirect' + }, + getWithPopupArgs: { + monitorPopupWindow: false, + }, + }) + .catch(err => { + expect(err).toBeInstanceOf(AuthSdkError); + expect(err.message).toBe('`timeout` must be set when `monitorPopupWindow` is set to `false`!'); + }); + + expect(intervalSpy).not.toHaveBeenCalled(); + }); + }); diff --git a/yarn.lock b/yarn.lock index b4fe94d26..131f2ba4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12066,7 +12066,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -12084,15 +12084,6 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -12153,14 +12144,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -13616,7 +13600,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -13642,15 +13626,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 101c92a2ccbbbf6ba1462d017387982ddfc66e22 Mon Sep 17 00:00:00 2001 From: Jared Perreault <jared.perreault@okta.com> Date: Fri, 17 Jan 2025 19:31:07 -0500 Subject: [PATCH 2/2] CHLOG --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71de06ba4..f3ed4257a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +# 7.10.2 + +### Other + +- [#1566](https://github.com/okta/okta-auth-js/pull/1566) feat: adds advance option `monitorPopupWindow` to `getWithPopup` + # 7.10.1 ### Other diff --git a/package.json b/package.json index 2bcc6d9de..0a3dcea98 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": true, "name": "@okta/okta-auth-js", "description": "The Okta Auth SDK", - "version": "7.10.1", + "version": "7.10.2", "homepage": "https://github.com/okta/okta-auth-js", "license": "Apache-2.0", "main": "build/cjs/exports/default.js",