@pega/react-sdk-overrides 8.23.11-debug3 → 8.23.11-debug4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/lib/helpers/auth.js +420 -852
  2. package/package.json +1 -1
  3. package/LICENSE +0 -201
@@ -1,929 +1,497 @@
1
- /* eslint-disable no-undef */
2
- export class PegaAuth {
3
- // The properties within config structure are expected to be more static config values that are then
4
- // used to properly make various OAuth endpoint calls.
5
- #config = null;
6
- // Any dynamic state is stored separately in its own structure. If a sessionStorage key is passed in
7
- // without a Dynamic State key.
8
- #dynState = {};
9
- // Current properties within dynState structure:
10
- // codeVerifier, state, sessionIndex, sessionIndexAttempts, acRedirectUri, silentAuthFailed
11
-
12
- constructor(ssKeyConfig, ssKeyDynState = '') {
13
- if (typeof ssKeyConfig === 'string') {
14
- this.ssKeyConfig = ssKeyConfig;
15
- this.ssKeyDynState = ssKeyDynState || `${ssKeyConfig}_DS`;
16
- this.#reloadConfig();
17
- } else {
18
- // object with config structure is passed in
19
- this.#config = ssKeyConfig;
20
- this.#dynState = ssKeyDynState || {};
21
- }
22
- this.urlencoded = 'application/x-www-form-urlencoded';
23
- this.tokenError = 'Token endpoint error';
24
- this.isNode = typeof window === 'undefined';
25
-
26
- // For isNode path the below attributes are initialized on first method invocation
27
- if (!this.isNode) {
28
- this.crypto = window.crypto;
29
- this.subtle = window.crypto.subtle;
30
- }
31
- if (Object.keys(this.#config).length > 0) {
32
- if (!this.#config.serverType) {
33
- this.#config.serverType = 'infinity';
34
- }
35
- } else {
36
- throw new Error('invalid config settings');
37
- }
38
- }
1
+ class PegaAuth {
39
2
 
40
- #reloadSS(ssKey) {
41
- const sItem = window.sessionStorage.getItem(ssKey);
3
+ constructor(ssKeyConfig) {
4
+ this.ssKeyConfig = ssKeyConfig;
5
+ this.bEncodeSI = false;
6
+ this.reloadConfig();
7
+ }
8
+
9
+ reloadConfig() {
10
+ const peConfig = window.sessionStorage.getItem(this.ssKeyConfig);
42
11
  let obj = {};
43
- if (sItem) {
44
- try {
45
- obj = JSON.parse(sItem);
46
- } catch (e) {
12
+ if( peConfig ) {
47
13
  try {
48
- obj = JSON.parse(atob(sItem));
49
- } catch (err) {
50
- obj = {};
14
+ obj = JSON.parse(peConfig);
15
+ } catch (e) {
16
+ try {
17
+ obj = JSON.parse(window.atob(peConfig));
18
+ } catch(e2) {
19
+ obj = {};
20
+ }
51
21
  }
52
- }
53
22
  }
54
- if (ssKey === this.ssKeyConfig) {
55
- this.#config = sItem ? obj : {};
56
- } else {
57
- this.#dynState = sItem ? obj : {};
58
- }
59
- }
60
-
61
- #reloadConfig() {
62
- if (this.ssKeyConfig) {
63
- this.#reloadSS(this.ssKeyConfig);
64
- }
65
- if (this.ssKeyDynState) {
66
- this.#reloadSS(this.ssKeyDynState);
67
- }
68
- }
69
-
70
- #updateConfig() {
71
- // transform must occur unless it is explicitly disabled
72
- const transform = this.#config.transform !== false;
73
- // May not need to write out Config info all the time, but there is a scenario where a
74
- // non obfuscated value is passed in and then it needs to be obfuscated
75
- if (this.ssKeyConfig) {
76
- const sConfig = JSON.stringify(this.#config);
77
- window.sessionStorage.setItem(this.ssKeyConfig, transform ? btoa(sConfig) : sConfig);
78
- }
79
- if (this.ssKeyDynState) {
80
- const sDynState = JSON.stringify(this.#dynState);
81
- window.sessionStorage.setItem(this.ssKeyDynState, transform ? btoa(sDynState) : sDynState);
82
- }
83
- if (this.#config.fnDynStateChangedCB) {
84
- this.#config.fnDynStateChangedCB();
85
- }
86
- }
87
23
 
88
- async #importSingleLib(libName, libProp, bLoadAlways = false) {
89
- if (!bLoadAlways && typeof (this.isNode ? global : window)[libProp] !== 'undefined') {
90
- this[libProp] = (this.isNode ? global : window)[libProp];
91
- return this[libProp];
92
- }
93
- // Needed to explicitly make import argument a string by using template literals to fix a compile
94
- // error: Critical dependency: the request of a dependency is an expression
95
- return import(/* webpackIgnore: true */ `${libName}`)
96
- .then((mod) => {
97
- this[libProp] = mod.default;
98
- })
99
- .catch((e) => {
100
- // eslint-disable-next-line no-console
101
- console.error(`Library ${libName} failed to load. ${e}`);
102
- throw e;
103
- });
104
- }
24
+ this.config = peConfig ? obj : null;
25
+ }
105
26
 
106
- async #importNodeLibs() {
107
- // Also current assumption is using Node 18 or better
108
- // With 18.3 there is now a native fetch (but may want to force use of node-fetch)
109
- const useNodeFetch = !!this.#config.useNodeFetch;
110
-
111
- return Promise.all([
112
- this.#importSingleLib('node-fetch', 'fetch', useNodeFetch),
113
- this.#importSingleLib('open', 'open'),
114
- this.#importSingleLib('node:crypto', 'crypto', true),
115
- this.#importSingleLib('node:https', 'https'),
116
- this.#importSingleLib('node:http', 'http'),
117
- this.#importSingleLib('node:fs', 'fs')
118
- ]).then(() => {
119
- this.subtle = this.crypto?.subtle || this.crypto.webcrypto.subtle;
120
- if ((typeof fetch === 'undefined' || useNodeFetch) && this.fetch) {
121
- /* eslint-disable-next-line no-global-assign */
122
- fetch = this.fetch;
123
- }
124
- });
125
- }
27
+ #updateConfig() {
28
+ const sSI = JSON.stringify(this.config);
29
+ window.sessionStorage.setItem(this.ssKeyConfig, this.bEncodeSI ? window.btoa(sSI) : sSI);
30
+ }
126
31
 
127
- // For PKCE the authorize includes a code_challenge & code_challenge_method as well
128
- async #buildAuthorizeUrl(state) {
129
- const {
130
- serverType,
131
- clientId,
132
- redirectUri,
133
- authorizeUri,
134
- userinfoUri,
135
- authService,
136
- appAlias,
137
- userIdentifier,
138
- password,
139
- noPKCE,
140
- isolationId
141
- } = this.#config;
142
- const { sessionIndex } = this.#dynState;
143
- const bInfinity = serverType === 'infinity';
144
-
145
- if (!noPKCE) {
146
- // Generate random string of 64 chars for verifier. RFC 7636 says from 43-128 chars
147
- const buf = new Uint8Array(64);
148
- this.crypto.getRandomValues(buf);
149
- this.#dynState.codeVerifier = this.#base64UrlSafeEncode(buf);
150
- }
32
+ // For PKCE the authorize includes a code_challenge & code_challenge_method as well
33
+ async #buildAuthorizeUrl(state) {
34
+ const {clientId, redirectUri, authorizeUri, authService, sessionIndex, appAlias, useLocking,
35
+ userIdentifier, password} = this.config;
151
36
 
37
+ // Generate random string of 64 chars for verifier. RFC 7636 says from 43-128 chars
38
+ let buf = new Uint8Array(64);
39
+ window.crypto.getRandomValues(buf);
40
+ this.config.codeVerifier = this.#base64UrlSafeEncode(buf);
152
41
  // If sessionIndex exists then increment attempts count (we will stop sending session_index after two failures)
153
- // With Infinity '24 we can now properly detect a invalid_session_index error, but can't for earlier versions
154
- if (sessionIndex) {
155
- this.#dynState.sessionIndexAttempts += 1;
42
+ if( sessionIndex ) {
43
+ this.config.sessionIndexAttempts += 1;
156
44
  }
157
-
158
- // We use state to verify that the received code is for the right authorize transaction
159
- this.#dynState.state = `${state || ''}.${this.#getRandomString(32)}`;
160
-
161
- // The same redirectUri needs to be provided to token endpoint, so save this away incase redirectUri is
162
- // adjusted for next authorize
163
- this.#dynState.acRedirectUri = redirectUri;
164
-
165
45
  // Persist codeVerifier in session storage so it survives the redirects that are to follow
166
46
  this.#updateConfig();
167
47
 
48
+ if( !state ) {
49
+ // Calc random state variable
50
+ buf = new Uint8Array(32);
51
+ window.crypto.getRandomValues(buf);
52
+ state = this.#base64UrlSafeEncode(buf);
53
+ }
54
+
168
55
  // Trim alias to include just the real alias piece
169
- const additionalScope =
170
- (userinfoUri ? '+profile' : '') + (appAlias ? `+app.alias.${appAlias.replace(/^app\//, '')}` : '');
171
- const scope = bInfinity ? `openid+email${additionalScope}` : 'user_info';
56
+ const addtlScope = appAlias ? `+app.alias.${appAlias.replace(/^app\//, '')}` : "";
172
57
 
173
58
  // Add explicit creds if specified to try to avoid login popup
174
- const authServiceArg = authService ? `&authentication_service=${encodeURIComponent(authService)}` : '';
175
- const sessionIndexArg =
176
- sessionIndex && this.#dynState.sessionIndexAttempts < 3 ? `&session_index=${sessionIndex}` : '';
177
- const userIdentifierArg = userIdentifier ? `&UserIdentifier=${encodeURIComponent(userIdentifier)}` : '';
178
- const passwordArg = password && userIdentifier ? `&Password=${encodeURIComponent(atob(password))}` : '';
179
- const isolationIdArg = !bInfinity && isolationId ? `&isolationID=${isolationId}` : '';
180
- const moreAuthArgs = bInfinity
181
- ? `&enable_psyncId=true&cookies=none${authServiceArg}${sessionIndexArg}${userIdentifierArg}${passwordArg}`
182
- : isolationIdArg;
183
-
184
- let pkceArgs = '';
185
- if (!noPKCE) {
186
- const cc = await this.#getCodeChallenge(this.#dynState.codeVerifier);
187
- pkceArgs = `&code_challenge=${cc}&code_challenge_method=S256`;
188
- }
189
- return `${authorizeUri}?client_id=${clientId}&response_type=code&redirect_uri=${redirectUri}&scope=${scope}&state=${encodeURIComponent(
190
- this.#dynState.state
191
- )}${pkceArgs}${moreAuthArgs}`;
192
- }
193
-
194
- async login() {
195
- if (this.isNode && !this.crypto) {
196
- // Deferring dynamic loading of node libraries til this first method to avoid doing this in constructor
197
- await this.#importNodeLibs();
198
- }
199
- const { grantType, noPKCE } = this.#config;
200
- if (grantType && grantType !== 'authCode') {
201
- return this.getToken();
202
- }
203
- // Make sure browser in a secure context, else PKCE will fail
204
- if (!this.isNode && !noPKCE && !window.isSecureContext) {
205
- throw new Error(
206
- `Authorization code grant flow failed due to insecure browser context at ${window.location.origin}. Use localhost or https.`
207
- );
208
- }
209
- return this.#authCodeStart();
210
- }
59
+ const moreAuthArgs =
60
+ (authService ? `&authentication_service=${encodeURIComponent(authService)}` : "") +
61
+ (sessionIndex && this.config.sessionIndexAttempts < 3 ? `&session_index=${sessionIndex}` : "") +
62
+ (useLocking ? `&enable_psyncId=true` : '') +
63
+ (userIdentifier ? `&UserIdentifier=${encodeURIComponent(userIdentifier)}` : '') +
64
+ (userIdentifier && password ? `&Password=${encodeURIComponent(window.atob(password))}` : '');
65
+
66
+ return this.#getCodeChallenge(this.config.codeVerifier).then( cc => {
67
+ // Now includes new enable_psyncId=true and session_index params
68
+ return `${authorizeUri}?client_id=${clientId}&response_type=code&redirect_uri=${redirectUri}&scope=openid+email+profile${addtlScope}&state=${state}&code_challenge=${cc}&code_challenge_method=S256${moreAuthArgs}`;
69
+ });
70
+ }
211
71
 
212
- // authCode login issues the authorize endpoint transaction and deals with redirects
213
- async #authCodeStart() {
72
+ async login() {
214
73
  const fnGetRedirectUriOrigin = () => {
215
- const redirectUri = this.#config.redirectUri;
216
- const nRootOffset = redirectUri.indexOf('//');
217
- const nFirstPathOffset = nRootOffset !== -1 ? redirectUri.indexOf('/', nRootOffset + 2) : -1;
218
- return nFirstPathOffset !== -1 ? redirectUri.substring(0, nFirstPathOffset) : redirectUri;
74
+ const redirectUri = this.config.redirectUri;
75
+ const nRootOffset = redirectUri.indexOf("//");
76
+ const nFirstPathOffset = nRootOffset !== -1 ? redirectUri.indexOf("/",nRootOffset+2) : -1;
77
+ return nFirstPathOffset !== -1 ? redirectUri.substring(0,nFirstPathOffset) : redirectUri;
219
78
  };
220
79
 
221
80
  const redirectOrigin = fnGetRedirectUriOrigin();
222
- const startState = this.isNode ? '' : btoa(window.location.origin);
223
-
224
- return new Promise((resolve, reject) => {
225
- let theUrl = null; // holds the crafted authorize url
226
-
227
- let myWindow = null; // popup or iframe
228
- let elIframe = null;
229
- let elCloseBtn = null;
230
- const iframeTimeout = this.#config.silentTimeout !== undefined ? this.#config.silentTimeout : 5000;
231
- let bWinIframe = true;
232
- let tmrAuthComplete = null;
233
- let checkWindowClosed = null;
234
- let bDisablePromptNone = false;
235
- const myWinOnLoad = () => {
236
- try {
237
- if (bWinIframe) {
238
- elIframe.contentWindow.postMessage({ type: 'PegaAuth' }, redirectOrigin);
239
- } else {
240
- myWindow.postMessage({ type: 'PegaAuth' }, redirectOrigin);
241
- }
242
- } catch (e) {
243
- // Exception trying to postMessage on load (perhaps should console.warn)
244
- }
245
- };
246
- const fnSetSilentAuthFailed = (bSet) => {
247
- this.#dynState.silentAuthFailed = bSet;
248
- this.#updateConfig();
249
- };
250
- /* eslint-disable prefer-promise-reject-errors */
251
- const fnOpenPopup = () => {
252
- if (this.#config.noPopups) {
253
- return reject('no-popups');
254
- }
255
- // Since displaying a visible window, clear the silent auth failed flag
256
- fnSetSilentAuthFailed(false);
257
- myWindow = (this.isNode ? this.open : window.open)(theUrl, '_blank', 'width=700,height=500,left=200,top=100');
258
- if (!myWindow) {
259
- // Blocked by popup-blocker
260
- return reject('blocked');
261
- }
262
- checkWindowClosed = setInterval(() => {
263
- if (myWindow.closed) {
264
- clearInterval(checkWindowClosed);
265
- reject('closed');
266
- }
267
- }, 500);
268
- if (!this.isNode) {
269
- try {
270
- myWindow.addEventListener('load', myWinOnLoad, true);
271
- } catch (e) {
272
- // Exception trying to add onload handler to opened window
273
- // eslint-disable-next-line no-console
274
- console.error(`Error adding event listener on popup window: ${e}`);
275
- }
276
- }
277
- };
278
-
279
- /* eslint-enable prefer-promise-reject-errors */
280
- const fnCloseIframe = () => {
281
- elIframe.parentNode.removeChild(elIframe);
282
- elCloseBtn.parentNode.removeChild(elCloseBtn);
283
- elIframe = null;
284
- elCloseBtn = null;
285
- bWinIframe = false;
286
- };
287
-
288
- const fnCloseAndReject = () => {
289
- fnCloseIframe();
290
- /* eslint-disable-next-line prefer-promise-reject-errors */
291
- reject('closed');
292
- };
293
-
294
- const fnAuthMessageReceiver = (event) => {
295
- // Check origin to make sure it is the redirect origin
296
- if (event.origin !== redirectOrigin) {
297
- if (event.data?.type === 'PegaAuth') {
298
- // eslint-disable-next-line no-console
299
- console.error(
300
- `Authorization code grant flow error: Unexpected origin: ${event.origin} ... expecting: ${redirectOrigin}`
301
- );
302
- }
303
- return;
304
- }
305
- if (!event.data || !event.data.type || event.data.type !== 'PegaAuth') return;
306
- const aArgs = ['code', 'state', 'error', 'errorDesc'];
307
- const aValues = [];
308
- for (let i = 0; i < aArgs.length; i += 1) {
309
- const arg = aArgs[i];
310
- aValues[arg] = event.data[arg] ? event.data[arg].toString() : null;
311
- }
312
- const { code, state, error, errorDesc } = aValues;
313
- if (error) {
314
- // eslint-disable-next-line no-console
315
- console.error(`Authorization code grant flow error (${error}): ${errorDesc}`);
316
- }
317
- if (code && state !== this.#dynState.state) {
318
- // eslint-disable-next-line no-console
319
- console.error(
320
- `Authorization code transfer error: state mismatch: ${state} ... expecting: ${this.#dynState.state}`
321
- );
322
- }
323
- if (error || (code && state === this.#dynState.state)) {
324
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
325
- fnGetTokenAndFinish(code, error, errorDesc);
326
- }
327
- };
328
-
329
- const fnEnableMessageReceiver = (bEnable) => {
330
- if (bEnable) {
331
- window.addEventListener('message', fnAuthMessageReceiver, false);
332
- window.authCodeCallback = (code, state, error, errorDesc) => {
333
- if (error) {
334
- // eslint-disable-next-line no-console
335
- console.error(`Authorization code grant flow error (${error}): ${errorDesc}`);
336
- }
337
- if (code && state !== this.#dynState.state) {
338
- // eslint-disable-next-line no-console
339
- console.error(
340
- `Authorization code transfer error: state mismatch: ${state} ... expecting: ${this.#dynState.state}`
341
- );
342
- }
343
- if (error || (code && state === this.#dynState.state)) {
344
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
345
- fnGetTokenAndFinish(code, error, errorDesc);
346
- }
347
- };
348
- } else {
349
- window.removeEventListener('message', fnAuthMessageReceiver, false);
350
- delete window.authCodeCallback;
351
- }
352
- };
353
-
354
- const doAuthorize = () => {
355
- // If there is a userIdentifier and password specified or an external SSO auth service,
356
- // we can try to use this silently in an iFrame first
357
- bWinIframe =
358
- !this.isNode &&
359
- !this.#dynState.silentAuthFailed &&
360
- iframeTimeout > 0 &&
361
- ((!!this.#config.userIdentifier && !!this.#config.password) ||
362
- this.#config.iframeLoginUI ||
363
- this.#config.authService !== 'pega');
364
- // Enable message receiver
365
- if (!this.isNode) {
366
- fnEnableMessageReceiver(true);
367
- }
368
- if (bWinIframe) {
369
- const nFrameZLevel = 99999;
370
- elIframe = document.createElement('iframe');
371
- elIframe.id = `pe${this.#config.clientId}`;
372
- const loginBoxWidth = 500;
373
- const loginBoxHeight = 700;
374
- const oStyle = elIframe.style;
375
- oStyle.position = 'absolute';
376
- oStyle.display = 'none';
377
- oStyle.zIndex = nFrameZLevel;
378
- oStyle.top = `${Math.round(Math.max(window.innerHeight - loginBoxHeight, 0) / 2)}px`;
379
- oStyle.left = `${Math.round(Math.max(window.innerWidth - loginBoxWidth, 0) / 2)}px`;
380
- oStyle.width = '500px';
381
- oStyle.height = '700px';
382
- // Add Iframe to top of document DOM to have it load
383
- document.body.insertBefore(elIframe, document.body.firstChild);
384
- // document.getElementsByTagName('body')[0].appendChild(elIframe);
385
- elIframe.addEventListener('load', myWinOnLoad, true);
386
- // Disallow iframe content attempts to navigate main window
387
- elIframe.setAttribute('sandbox', 'allow-scripts allow-forms allow-same-origin');
388
- // Adding prompt=none as this is standard OIDC way to communicate no UI is expected (With Infinity '23 or better, this is passed on to
389
- // configured OIDC authentication services). cookies=none disables the temporary Pega-Rules cookie otherwise created on auth code
390
- // grant flow. For now these two args are either both set or not set, but might have a cookies="partitioned" one day.
391
- elIframe.setAttribute('src', bDisablePromptNone ? theUrl : `${theUrl}&prompt=none`);
392
-
393
- const svgCloseBtn = `<?xml version="1.0" encoding="UTF-8"?>
394
- <svg width="34px" height="34px" viewBox="0 0 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
395
- <title>Dismiss - Black</title>
396
- <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
397
- <g transform="translate(1.000000, 1.000000)">
398
- <circle fill="#252C32" cx="16" cy="16" r="16"></circle>
399
- <g transform="translate(9.109375, 9.214844)" fill="#FFFFFF" fill-rule="nonzero">
400
- <path d="M12.7265625,0 L0,12.6210938 L1.0546875,13.5703125 L13.78125,1.0546875 L12.7265625,0 Z M13.7460938,12.5507812 L1.01953125,0 L0,1.01953125 L12.7617188,13.6054688 L13.7460938,12.5507812 Z"></path>
401
- </g>
402
- </g>
403
- </g>
404
- </svg>`;
405
- const bCloseWithinFrame = false;
406
- elCloseBtn = document.createElement('img');
407
- elCloseBtn.onclick = fnCloseAndReject;
408
- elCloseBtn.src = `data:image/svg+xml;base64,${btoa(svgCloseBtn)}`;
409
- const oBtnStyle = elCloseBtn.style;
410
- oBtnStyle.cursor = 'pointer';
411
- // If svg doesn't set width and height might want to set oBtStyle width and height to something like '2em'
412
- oBtnStyle.position = 'absolute';
413
- oBtnStyle.display = 'none';
414
- oBtnStyle.zIndex = nFrameZLevel + 1;
415
- const nTopOffset = bCloseWithinFrame ? 5 : -10;
416
- const nRightOffset = bCloseWithinFrame ? -34 : -20;
417
- const nTop = Math.round(Math.max(window.innerHeight - loginBoxHeight, 0) / 2) + nTopOffset;
418
- oBtnStyle.top = `${nTop}px`;
419
- const nLeft = Math.round(Math.max(window.innerWidth - loginBoxWidth, 0) / 2) + loginBoxWidth + nRightOffset;
420
- oBtnStyle.left = `${nLeft}px`;
421
- document.body.insertBefore(elCloseBtn, document.body.firstChild);
422
- // If the password was wrong, then the login screen will be in the iframe
423
- // ..and with Pega without realization of US-372314 it may replace the top (main portal) window
424
- // For now set a timer and if the timer expires, remove the iFrame and use same url within
425
- // visible window
426
- tmrAuthComplete = setTimeout(() => {
427
- clearTimeout(tmrAuthComplete);
428
- /*
429
- // remove password from config
430
- if (this.#config.password) {
431
- delete this.#config.password;
432
- this.#updateConfig();
433
- }
434
- */
435
- // Display the iframe where the redirects did not succeed (or invoke a popup window)
436
- if (this.#config.iframeLoginUI) {
437
- elIframe.style.display = 'block';
438
- elCloseBtn.style.display = 'block';
439
- } else {
440
- fnCloseIframe();
441
- fnOpenPopup();
442
- }
443
- }, iframeTimeout);
444
- } else {
445
- if (this.isNode) {
446
- // Determine port to listen to by extracting it from redirect uri
447
- const { redirectUri, cert, key } = this.#config;
448
- const isHttp = redirectUri.startsWith('http:');
449
- const nLocalhost = redirectUri.indexOf('localhost:');
450
- const nSlash = redirectUri.indexOf('/', nLocalhost + 10);
451
- const nPort = parseInt(redirectUri.substring(nLocalhost + 10, nSlash), 10);
452
- if (nLocalhost !== -1) {
453
- const options =
454
- key && cert && !isHttp
455
- ? {
456
- key: this.fs.readFileSync(key),
457
- cert: this.fs.readFileSync(cert)
81
+ // eslint-disable-next-line no-restricted-globals
82
+ const state = window.btoa(location.origin);
83
+
84
+ return new Promise( (resolve, reject) => {
85
+
86
+ this.#buildAuthorizeUrl(state).then((url) => {
87
+ let myWindow = null; // popup or iframe
88
+ let elIframe = null;
89
+ let elCloseBtn = null
90
+ const iframeTimeout = this.config.silentTimeout !== undefined ? this.config.silentTimeout : 5000;
91
+ let bWinIframe = iframeTimeout > 0 && ((!!this.config.userIdentifier && !!this.config.password) || this.config.iframeLoginUI || this.config.authService !== "pega");
92
+ let tmrAuthComplete = null;
93
+ let checkWindowClosed = null;
94
+ const myWinOnLoad = () => {
95
+ try{
96
+ if( bWinIframe ) {
97
+ // eslint-disable-next-line no-console
98
+ console.log("authjs(login): loaded a page in iFrame");
99
+ elIframe.contentWindow.postMessage({type:"PegaAuth"}, redirectOrigin);
100
+ } else {
101
+ // eslint-disable-next-line no-console
102
+ console.log(`authjs(login): loaded a page in popup window...sending 'PegaAuth' message. redirectOrigin: ${redirectOrigin}`);
103
+ myWindow.postMessage({type:"PegaAuth"}, redirectOrigin);
458
104
  }
459
- : {};
460
- const server = (isHttp ? this.http : this.https).createServer(options, (req, res) => {
461
- const { winTitle, winBodyHtml } = this.#config;
462
- res.writeHead(200, { 'Content-Type': 'text/html' });
463
- // Auto closing window for now. Can always leave it up and allow authConfig props to set title and bodyHtml
464
- res.end(
465
- `<html><head><title>${winTitle}</title><script>window.close();</script></head><body>${winBodyHtml}</body></html>`
466
- );
467
- const queryString = req.url.split('?')[1];
468
- const urlParams = new URLSearchParams(queryString);
469
- const code = urlParams.get('code');
470
- const state = urlParams.get('state');
471
- const error = urlParams.get('error');
472
- const errorDesc = urlParams.get('error_description');
473
- if (error || (code && state === this.#dynState.state)) {
474
- // Stop receiving connections and close when all are handled.
475
- server.close();
476
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
477
- fnGetTokenAndFinish(code, error, errorDesc);
105
+ } catch(e) {
106
+ // eslint-disable-next-line no-console
107
+ console.log("authjs(login): Exception trying to postMessage on load");
478
108
  }
479
- });
480
- server.listen(nPort);
481
- }
482
- }
483
- fnOpenPopup();
484
- }
485
- };
486
-
487
- /* Retrieve token(s) and close login window */
488
- const fnGetTokenAndFinish = (code, error, errorDesc) => {
489
- // Can clear state in session info at this point
490
- delete this.#dynState.state;
491
- this.#updateConfig();
109
+ };
110
+ const fnOpenPopup = () => {
111
+ myWindow = window.open(url, '_blank', 'width=700,height=500,left=200,top=100');
112
+ if( !myWindow ) {
113
+ // Blocked by popup-blocker
114
+ // eslint-disable-next-line prefer-promise-reject-errors
115
+ return reject("blocked");
116
+ }
117
+ checkWindowClosed = setInterval( () => {
118
+ if( myWindow.closed ) {
119
+ clearInterval(checkWindowClosed);
120
+ // eslint-disable-next-line prefer-promise-reject-errors
121
+ reject("closed");
122
+ }
123
+ myWindow.postMessage({type:"PegaAuth"}, redirectOrigin);
124
+ }, 500);
125
+ try {
126
+ myWindow.addEventListener("load", myWinOnLoad, true);
127
+ } catch(e) {
128
+ // eslint-disable-next-line no-console
129
+ console.log("authjs(login): Exception trying to add onload handler to opened window;")
130
+ }
131
+ };
132
+
133
+ const fnCloseIframe = () => {
134
+ elIframe.parentNode.removeChild(elIframe);
135
+ elCloseBtn.parentNode.removeChild(elCloseBtn);
136
+ // eslint-disable-next-line no-multi-assign
137
+ elIframe = elCloseBtn = null;
138
+ bWinIframe = false;
139
+ };
140
+ const fnCloseAndReject = () => {
141
+ fnCloseIframe();
142
+ // eslint-disable-next-line prefer-promise-reject-errors
143
+ reject("closed");
144
+ };
145
+ // If there is a userIdentifier and password specified or an external SSO auth service,
146
+ // we can try to use this silently in an iFrame first
147
+ if( bWinIframe ) {
148
+ const nFrameZLevel = 99999;
149
+ elIframe = document.createElement('iframe');
150
+ // eslint-disable-next-line prefer-template
151
+ elIframe.id = 'pe'+this.config.clientId;
152
+ const loginBoxWidth=500;
153
+ const loginBoxHeight=700;
154
+ const oStyle = elIframe.style;
155
+ oStyle.position = 'absolute';
156
+ oStyle.display = 'none';
157
+ oStyle.zIndex = nFrameZLevel;
158
+ oStyle.top=`${Math.round(Math.max(window.innerHeight-loginBoxHeight,0)/2)}px`;
159
+ oStyle.left=`${Math.round(Math.max(window.innerWidth-loginBoxWidth,0)/2)}px`;
160
+ oStyle.width='500px';
161
+ oStyle.height='700px';
162
+ // Add Iframe to top of document DOM to have it load
163
+ document.body.insertBefore(elIframe,document.body.firstChild);
164
+ // Add Iframe to DOM to have it load
165
+ document.getElementsByTagName('body')[0].appendChild(elIframe);
166
+ elIframe.addEventListener("load", myWinOnLoad, true);
167
+ // Disallow iframe content attempts to navigate main window
168
+ elIframe.setAttribute("sandbox","allow-scripts allow-forms allow-same-origin");
169
+ elIframe.setAttribute('src', url);
170
+
171
+ const svgCloseBtn =
172
+ `<?xml version="1.0" encoding="UTF-8"?>
173
+ <svg width="34px" height="34px" viewBox="0 0 34 34" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
174
+ <title>Dismiss - Black</title>
175
+ <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
176
+ <g transform="translate(1.000000, 1.000000)">
177
+ <circle fill="#252C32" cx="16" cy="16" r="16"></circle>
178
+ <g transform="translate(9.109375, 9.214844)" fill="#FFFFFF" fill-rule="nonzero">
179
+ <path d="M12.7265625,0 L0,12.6210938 L1.0546875,13.5703125 L13.78125,1.0546875 L12.7265625,0 Z M13.7460938,12.5507812 L1.01953125,0 L0,1.01953125 L12.7617188,13.6054688 L13.7460938,12.5507812 Z"></path>
180
+ </g>
181
+ </g>
182
+ </g>
183
+ </svg>`;
184
+ const bCloseWithinFrame = false;
185
+ elCloseBtn = document.createElement('img');
186
+ elCloseBtn.onclick = fnCloseAndReject;
187
+ // eslint-disable-next-line prefer-template
188
+ elCloseBtn.src = 'data:image/svg+xml;base64,' + window.btoa(svgCloseBtn);
189
+ const oBtnStyle = elCloseBtn.style;
190
+ oBtnStyle.cursor = 'pointer';
191
+ // If svg doesn't set width and height might want to set oBtStyle width and height to something like '2em'
192
+ oBtnStyle.position = 'absolute';
193
+ oBtnStyle.display = 'none';
194
+ oBtnStyle.zIndex = nFrameZLevel+1;
195
+ const nTopOffset = bCloseWithinFrame ? 5 : -10;
196
+ const nRightOffset = bCloseWithinFrame ? -34 : -20;
197
+ oBtnStyle.top = `${Math.round(Math.max(window.innerHeight-loginBoxHeight,0)/2)+nTopOffset}px`;
198
+ oBtnStyle.left = `${Math.round(Math.max(window.innerWidth-loginBoxWidth,0)/2)+loginBoxWidth+nRightOffset}px`;
199
+ document.body.insertBefore(elCloseBtn,document.body.firstChild);
200
+ // If the password was wrong, then the login screen will be in the iframe
201
+ // ..and with Pega without realization of US-372314 it may replace the top (main portal) window
202
+ // For now set a timer and if the timer expires, remove the iFrame and use same url within
203
+ // visible window
204
+ tmrAuthComplete = setTimeout( () => {
205
+ clearTimeout(tmrAuthComplete);
206
+ // remove password from config
207
+ if( this.config.password ) {
208
+ delete this.config.password;
209
+ this.#updateConfig();
210
+ }
492
211
 
493
- if (!this.isNode) {
494
- fnEnableMessageReceiver(false);
495
- if (bWinIframe) {
496
- clearTimeout(tmrAuthComplete);
497
- fnCloseIframe();
498
- } else {
499
- clearInterval(checkWindowClosed);
500
- try {
501
- if (myWindow) {
502
- myWindow.close();
503
- myWindow = null;
504
- }
505
- } catch (e) {
506
- // keep going and process code or error even though issue closing popup
507
- // eslint-disable-next-line no-console
508
- console.warn(`Error closing window: ${e}`);
509
- }
510
- }
511
- }
512
- if (code) {
513
- this.getToken(code)
514
- .then((token) => {
515
- resolve(token);
516
- })
517
- .catch((e) => {
518
- reject(e);
519
- });
520
- } else if (error) {
521
- // Handle some errors in a special manner and pass others back to client
522
- if (error === 'login_required') {
523
- // eslint-disable-next-line no-console
524
- console.warn('silent authentication failed...starting full authentication');
525
- const bSpecialDebugPath = false;
526
- if (bSpecialDebugPath) {
527
- fnSetSilentAuthFailed(false);
528
- bDisablePromptNone = true;
212
+ if( this.config.iframeLoginUI ) {
213
+ elIframe.style.display="block";
214
+ elCloseBtn.style.display="block";
215
+ } else {
216
+ fnCloseIframe();
217
+ // eslint-disable-next-line no-console
218
+ console.log(`Opening popup inside tmrAuthComplete (bWinIframe: ${bWinIframe})`);
219
+ fnOpenPopup();
220
+ }
221
+ }, iframeTimeout);
529
222
  } else {
530
- fnSetSilentAuthFailed(true);
531
- bDisablePromptNone = false;
223
+ // eslint-disable-next-line no-console
224
+ console.log(`Opening popup outside tmrAuthComplete (bWinIframe: ${bWinIframe})`);
225
+ fnOpenPopup();
532
226
  }
533
- this.#buildAuthorizeUrl(startState).then((url) => {
534
- theUrl = url;
535
- doAuthorize();
536
- });
537
- } else if (error === 'invalid_session_index') {
538
- // eslint-disable-next-line no-console
539
- console.warn('auth session no longer valid...starting new session');
540
- // In these scenarios, not much user can do without just starting a new session, so do that
541
- this.#updateSessionIndex(null);
542
- fnSetSilentAuthFailed(false);
543
- this.#buildAuthorizeUrl(startState).then((url) => {
544
- theUrl = url;
545
- doAuthorize();
546
- });
547
- } else {
548
- // eslint-disable-next-line no-console
549
- console.warn(`Authorize failed: ${error}. ${errorDesc}\nFailing authorize url: ${theUrl}`);
550
- throw new Error(error, { cause: errorDesc });
551
- }
552
- }
553
- };
554
227
 
555
- this.#buildAuthorizeUrl(startState).then((url) => {
556
- theUrl = url;
557
- doAuthorize();
558
- });
228
+ let authMessageReceiver = null;
229
+ /* Retrieve token(s) and close login window */
230
+ const fnGetTokenAndFinish = (code) => {
231
+ window.removeEventListener("message", authMessageReceiver, false);
232
+ this.getToken(code).then(token => {
233
+ if( bWinIframe ) {
234
+ clearTimeout(tmrAuthComplete);
235
+ fnCloseIframe();
236
+ } else {
237
+ clearInterval(checkWindowClosed);
238
+ try {
239
+ myWindow.close();
240
+ } catch (e) {
241
+ // eslint-disable-next-line no-console
242
+ console.warn(`attempt to close opened window failed`);
243
+ }
244
+ }
245
+ resolve(token);
246
+ })
247
+ .catch(e => {
248
+ reject(e);
249
+ });
250
+ };
251
+ /* Handler to receive the auth code */
252
+ authMessageReceiver = (event) => {
253
+ // Check origin to make sure it is the redirect origin
254
+ if( event.origin !== redirectOrigin ) {
255
+ // eslint-disable-next-line no-console
256
+ console.info(`Received event from unexpected origin: ${event.origin} (was expecting: ${redirectOrigin})`);
257
+ }
258
+ if( !event.data || !event.data.type || event.data.type !== "PegaAuth" )
259
+ return;
260
+ // eslint-disable-next-line no-console
261
+ console.log("authjs(login): postMessage received with code");
262
+ const code = event.data.code.toString();
263
+ fnGetTokenAndFinish(code);
264
+ };
265
+ window.addEventListener("message", authMessageReceiver, false);
266
+ window.authCodeCallback = (code) => {
267
+ // eslint-disable-next-line no-console
268
+ console.log("authjs(login): authCodeCallback used with code");
269
+ fnGetTokenAndFinish(code);
270
+ };
271
+ });
559
272
  });
560
- }
273
+ }
561
274
 
562
- // Login redirect
563
- loginRedirect() {
275
+ // Login redirect
276
+ loginRedirect() {
564
277
  // eslint-disable-next-line no-restricted-globals
565
278
  const state = btoa(location.origin);
566
279
  this.#buildAuthorizeUrl(state).then((url) => {
567
- // eslint-disable-next-line no-restricted-globals
568
- location.href = url;
280
+ // eslint-disable-next-line no-restricted-globals
281
+ location.href = url;
569
282
  });
570
- }
571
-
572
- // check state
573
- checkStateMatch(state) {
574
- return state === this.#dynState.state;
575
- }
576
-
577
- // Clear session index within config
578
- #updateSessionIndex(sessionIndex) {
579
- if (sessionIndex) {
580
- this.#dynState.sessionIndex = sessionIndex;
581
- this.#dynState.sessionIndexAttempts = 0;
582
- } else if (this.#dynState.sessionIndex) {
583
- delete this.#dynState.sessionIndex;
584
- }
585
- this.#updateConfig();
586
- }
283
+ }
587
284
 
588
- // For PKCE token endpoint includes code_verifier
589
285
 
590
- getToken(authCode) {
286
+ // For PKCE token endpoint includes code_verifier
287
+ getToken(authCode) {
591
288
  // Reload config to pick up the previously stored codeVerifier
592
- this.#reloadConfig();
593
-
594
- const {
595
- serverType,
596
- isolationId,
597
- clientId,
598
- clientSecret,
599
- tokenUri,
600
- grantType,
601
- customTokenParams,
602
- userIdentifier,
603
- password,
604
- noPKCE,
605
- secureCookie
606
- } = this.#config;
607
-
608
- const { sessionIndex, acRedirectUri, codeVerifier } = this.#dynState;
609
-
610
- const bAuthCode = !grantType || grantType === 'authCode';
611
- if (bAuthCode && !authCode && !this.isNode) {
612
- const queryString = window.location.search;
613
- const urlParams = new URLSearchParams(queryString);
614
- authCode = urlParams.get('code');
615
- }
289
+ this.reloadConfig();
616
290
 
617
- const formData = new URLSearchParams();
618
- formData.append('client_id', clientId);
619
- if (clientSecret) {
620
- formData.append('client_secret', clientSecret);
621
- }
622
- if (secureCookie) {
623
- formData.append('send_token_as_cookie', 'true');
624
- }
291
+ const {clientId, clientSecret, redirectUri, tokenUri, codeVerifier} = this.config;
625
292
 
626
- const fullGTName = {
627
- authCode: 'authorization_code',
628
- clientCreds: 'client_credentials',
629
- customBearer: 'custom-bearer',
630
- passwordCreds: 'password'
631
- }[grantType];
632
- formData.append('grant_type', fullGTName || grantType || 'authorization_code');
633
- if (serverType === 'launchpad' && grantType !== 'authCode' && isolationId) {
634
- formData.append('isolation_ids', isolationId);
635
- }
636
- if (bAuthCode) {
637
- formData.append('code', authCode);
638
- formData.append('redirect_uri', acRedirectUri);
639
- if (!noPKCE) {
640
- formData.append('code_verifier', codeVerifier);
641
- }
642
- } else if (sessionIndex) {
643
- formData.append('session_index', sessionIndex);
644
- }
293
+ // eslint-disable-next-line no-restricted-globals
294
+ const queryString = location.search;
295
+ const urlParams = new URLSearchParams(queryString);
296
+ const code = authCode || urlParams.get("code");
645
297
 
646
- if (grantType === 'customBearer' && customTokenParams) {
647
- Object.keys(customTokenParams).forEach((param) => {
648
- formData.append(param, customTokenParams[param]);
649
- });
650
- }
651
- if (grantType !== 'authCode') {
652
- formData.append('enable_psyncId', 'true');
653
- }
654
- if (grantType === 'passwordCreds') {
655
- formData.append('username', userIdentifier);
656
- formData.append('password', atob(password));
298
+ const formData = new URLSearchParams();
299
+ formData.append("client_id", clientId);
300
+ if( clientSecret ) {
301
+ formData.append("client_secret", clientSecret);
657
302
  }
303
+ formData.append("grant_type", "authorization_code");
304
+ formData.append("code", code);
305
+ formData.append("redirect_uri", redirectUri);
306
+ formData.append("code_verifier", codeVerifier);
658
307
 
659
308
  return fetch(tokenUri, {
660
- agent: this.#getAgent(),
661
- method: 'POST',
309
+ method: "POST",
662
310
  headers: new Headers({
663
- 'content-type': this.urlencoded
311
+ "content-type": "application/x-www-form-urlencoded",
664
312
  }),
665
- credentials: secureCookie ? 'include' : 'omit',
666
- body: formData.toString()
313
+
314
+ body: formData.toString(),
667
315
  })
668
- .then((response) => {
669
- if (response.ok) {
670
- return response.json();
316
+ .then((response) => response.json())
317
+ .then(token => {
318
+ // .expires_in contains the # of seconds before access token expires
319
+ // add property to keep track of current time when the token expires
320
+ token.eA = Date.now() + (token.expires_in * 1000);
321
+ if( this.config.codeVerifier ) {
322
+ delete this.config.codeVerifier;
671
323
  }
672
- throw Error(`${this.tokenError}.`, { cause: response.status });
673
- })
674
- .then((token) => {
675
- if (token.errors || token.error) {
676
- // eslint-disable-next-line no-console
677
- console.error(`${this.tokenError}: ${JSON.stringify(token.errors || token.error)}`);
678
- } else {
679
- // .expires_in contains the # of seconds before access token expires
680
- // add property to keep track of current time when the token expires
681
- token.eA = Date.now() + token.expires_in * 1000;
682
- // Clear authCode related config state: state, codeVerifier, acRedirectUri
683
- if (this.#dynState.state) {
684
- delete this.#dynState.state;
685
- }
686
- if (this.#dynState.codeVerifier) {
687
- delete this.#dynState.codeVerifier;
688
- }
689
- if (this.#dynState.acRedirectUri) {
690
- delete this.#dynState.acRedirectUri;
691
- }
692
- // If there is a session_index then move this to the peConfig structure (as used on authorize)
693
- if (token.session_index) {
694
- this.#updateSessionIndex(token.session_index);
695
- // does an #updateConfig within #updateSessionIndex
696
- } else {
697
- this.#updateConfig();
698
- }
324
+ // If there is a session_index then move this to the peConfig structure (as used on authorize)
325
+ if( token.session_index ) {
326
+ this.config.sessionIndex = token.session_index;
699
327
  }
328
+ // If we got a token and have a session index, then reset the sessionIndexAttempts
329
+ if( this.config.sessionIndex ) {
330
+ this.config.sessionIndexAttempts = 0;
331
+ }
332
+ this.#updateConfig();
700
333
  return token;
701
- })
702
- .catch((e) => {
703
- const msg = `${e}`;
704
- // eslint-disable-next-line no-console
705
- console.error(msg.indexOf(this.tokenError) !== -1 ? msg : `${this.tokenError}: ${msg}`);
706
- throw e;
707
- });
708
- }
709
-
710
- async refreshToken(refreshToken) {
711
- const { clientId, clientSecret, tokenUri, secureCookie } = this.#config;
712
-
713
- if (this.isNode && !this.crypto) {
714
- // Deferring dynamic loading of node libraries til this first method to avoid doing this in constructor
715
- await this.#importNodeLibs();
716
- }
334
+ })
335
+ .catch(e => {
336
+ // eslint-disable-next-line no-console
337
+ console.log(e)
338
+ });
339
+ }
717
340
 
718
- if (!secureCookie && !refreshToken) {
719
- return null;
720
- }
341
+ /* eslint-disable camelcase */
342
+ async refreshToken(refresh_token) {
343
+ const {clientId, clientSecret, tokenUri} = this.config;
721
344
 
722
345
  const formData = new URLSearchParams();
723
- formData.append('client_id', clientId);
724
- if (clientSecret) {
725
- formData.append('client_secret', clientSecret);
726
- }
727
- formData.append('grant_type', 'refresh_token');
728
- if (secureCookie) {
729
- formData.append('send_token_as_cookie', 'true');
730
- }
731
- if (refreshToken) {
732
- formData.append('refresh_token', refreshToken);
346
+ formData.append("client_id", clientId);
347
+ if( clientSecret ) {
348
+ formData.append("client_secret", clientSecret);
733
349
  }
350
+ formData.append("grant_type", "refresh_token");
351
+ formData.append("refresh_token", refresh_token);
734
352
 
735
353
  return fetch(tokenUri, {
736
- agent: this.#getAgent(),
737
- method: 'POST',
354
+ method: "POST",
738
355
  headers: new Headers({
739
- 'content-type': this.urlencoded
356
+ "content-type": "application/x-www-form-urlencoded",
740
357
  }),
741
- credentials: secureCookie ? 'include' : 'omit',
742
- body: formData.toString()
358
+
359
+ body: formData.toString(),
743
360
  })
744
- .then((response) => {
745
- if (!response.ok && response.status === 401) {
361
+ .then((response) => {
362
+ if( !response.ok && response.status === 401 ) {
746
363
  return null;
747
- }
748
- return response.json();
749
- })
750
- .then((token) => {
751
- if (token) {
752
- // .expires_in contains the # of seconds before access token expires
753
- // add property to keep track of current time when the token expires
754
- token.eA = Date.now() + token.expires_in * 1000;
364
+ }
365
+ return response.json();
366
+ })
367
+ .then(token => {
368
+ if( token ) {
369
+ // .expires_in contains the # of seconds before access token expires
370
+ // add property to keep track of current time when the token expires
371
+ token.eA = Date.now() + (token.expires_in * 1000);
755
372
  }
756
373
  return token;
757
- })
758
- .catch((e) => {
759
- // eslint-disable-next-line no-console
760
- console.warn(`Refresh token failed: ${e}`);
761
- return null;
762
- });
763
- }
764
-
765
- /**
766
- * Revokes tokens (useful as part of a logout operation). For non-secureCookie, the accessToken and
767
- * the optional refreshToken would be passed in and the routine will generate two separate revoke
768
- * transactions. For secureCookie scenario, will issue just one for the accessToken, but this will
769
- * also revoke the refresh token if present.
770
- * @param {string} accessToken - the access token (or any string value for secureCookie scenario)
771
- * @param {string} refreshToken - optional refresh token (or any string value for non secureCookie
772
- * scenario, when a refreshToken exists)
773
- * @returns
774
- */
775
- async revokeTokens(accessToken, refreshToken = null) {
776
- if (Object.keys(this.#config).length === 0) {
777
- // Must have a config structure to proceed
778
- return;
779
- }
780
- const { clientId, clientSecret, revokeUri, secureCookie } = this.#config;
781
-
782
- if (this.isNode && !this.crypto) {
783
- // Deferring dynamic loading of node libraries til this first method to avoid doing this in constructor
784
- await this.#importNodeLibs();
785
- }
374
+ })
375
+ .catch(e => {
376
+ // eslint-disable-next-line no-console
377
+ console.log(e)
378
+ });
379
+ }
786
380
 
787
- const headers = { 'content-type': this.urlencoded };
788
- if (clientSecret) {
789
- const basicCreds = btoa(`${clientId}:${clientSecret}`);
790
- headers.authorization = `Basic ${basicCreds}`;
381
+ async revokeTokens(access_token, refresh_token = null) {
382
+ if( !this.config || !this.config.revokeUri) {
383
+ // Must have a config structure and revokeUri to proceed
384
+ return;
791
385
  }
386
+ const {clientId, clientSecret, revokeUri} = this.config;
792
387
 
793
- // A revoke of a refresh_token will also revoke any associated access_tokens, so only one
794
- // transaction is necessary.
795
- const prop = refreshToken ? 'refresh_token' : 'access_token';
796
- const formData = new URLSearchParams();
797
- if (!clientSecret) {
798
- formData.append('client_id', clientId);
799
- }
800
- if (secureCookie) {
801
- formData.append('send_token_as_cookie', 'true');
388
+ const hdrs = {"content-type":"application/x-www-form-urlencoded"};
389
+ if( clientSecret ) {
390
+ const creds = `${clientId}:${clientSecret}`;
391
+ hdrs.authorization = `Basic ${window.btoa(creds)}`;
802
392
  }
803
- const token = prop === 'access_token' ? accessToken : refreshToken;
804
- if (!secureCookie && token) {
805
- formData.append('token', token);
393
+ const aTknProps = ["access_token"];
394
+ if( refresh_token ) {
395
+ aTknProps.push("refresh_token");
806
396
  }
807
- formData.append('token_type_hint', prop);
808
- return fetch(revokeUri, {
809
- agent: this.#getAgent(),
810
- method: 'POST',
811
- headers: new Headers(headers),
812
- credentials: secureCookie ? 'include' : 'omit',
813
- body: formData.toString()
814
- })
815
- .then((response) => {
816
- if (!response.ok) {
817
- // eslint-disable-next-line no-console
818
- console.error(`Error revoking ${prop}:${response.status}`);
397
+ aTknProps.forEach( (prop) => {
398
+ const formData = new URLSearchParams();
399
+ if( !clientSecret ) {
400
+ formData.append("client_id", clientId);
819
401
  }
820
- })
821
- .catch((e) => {
822
- // eslint-disable-next-line no-console
823
- console.error(`Error revoking ${prop}; ${e}`);
824
- })
825
- .finally(() => {
826
- this.#dynState.silentAuthFailed = false;
827
- // Also clobber any sessionIndex
828
- this.#updateSessionIndex(null);
829
- });
830
- }
831
-
832
- // For userinfo endpoint to return meaningful data, endpoint must include appAlias (if specified) and authorize must
833
- // specify profile and optionally email scope to get such info returned
834
- async getUserinfo(accessToken) {
835
- if (!this.#config || !this.#config.userinfoUri) {
836
- // Must have a config structure and userInfo to proceed
837
- return {};
402
+ formData.append("token", prop==="access_token" ? access_token : refresh_token);
403
+ formData.append("token_type_hint", prop);
404
+ fetch(revokeUri, {
405
+ method: "POST",
406
+ headers: new Headers(hdrs),
407
+ body: formData.toString(),
408
+ })
409
+ .then((response) => {
410
+ if( !response.ok ) {
411
+ // eslint-disable-next-line no-console
412
+ console.log( `Error revoking ${prop}:${response.status}` );
413
+ }
414
+ })
415
+ .catch(e => {
416
+ // eslint-disable-next-line no-console
417
+ console.log(e);
418
+ });
419
+ } );
420
+ // Also clobber any sessionIndex
421
+ if( this.config.sessionIndex ) {
422
+ delete this.config.sessionIndex;
423
+ this.#updateConfig();
838
424
  }
839
- const headers = {
840
- authorization: `bearer ${accessToken}`,
841
- 'content-type': 'application/json;charset=UTF-8'
842
- };
843
- return fetch(this.#config.userinfoUri, {
844
- agent: this.#getAgent(),
845
- method: 'GET',
846
- headers: new Headers(headers)
847
- })
848
- .then((response) => {
849
- if (response.ok) {
850
- return response.json();
851
- }
852
- // eslint-disable-next-line no-console
853
- console.log(`Error invoking userinfo: ${response.status}`);
854
- })
855
- .then((data) => {
856
- return data;
857
- })
858
- .catch((e) => {
859
- // eslint-disable-next-line no-console
860
- console.log(e);
861
- });
862
- }
425
+ }
863
426
 
864
- #sha256Hash(str) {
865
- // Found that the Node implementation of subtle.digest is yielding incorrect results
866
- // so using a different set of apis to get expected results.
867
- if (this.isNode) {
868
- return new Promise((resolve) => {
869
- resolve(this.crypto.createHash('sha256').update(str).digest());
870
- });
871
- }
872
- return this.subtle.digest('SHA-256', new TextEncoder().encode(str));
427
+ // For userinfo endpoint to return meaningful data, endpoint must include appAlias (if specified) and authorize must
428
+ // specify profile and optionally email scope to get such info returned
429
+ async getUserinfo(access_token) {
430
+ if( !this.config || !this.config.userinfoUri ) {
431
+ // Must have a config structure and userInfo to proceed
432
+ return {};
873
433
  }
434
+ const hdrs = {'authorization':`bearer ${access_token}`,'content-type':'application/json;charset=UTF-8'};
435
+ return fetch(this.config.userinfoUri, {
436
+ method: "GET",
437
+ headers: new Headers(hdrs)})
438
+ .then( response => {
439
+ if( response.ok) {
440
+ return response.json();
441
+ } else {
442
+ // eslint-disable-next-line no-console
443
+ console.log( `Error invoking userinfo: ${response.status}` );
444
+ }
445
+ })
446
+ .then( data => {
447
+ return data;
448
+ })
449
+ .catch(e => {
450
+ // eslint-disable-next-line no-console
451
+ console.log(e);
452
+ });
453
+ }
874
454
 
875
- // Base64 encode
876
- /* eslint-disable-next-line class-methods-use-this */
877
- #encode64(buff) {
878
- return btoa(new Uint8Array(buff).reduce((s, b) => s + String.fromCharCode(b), ''));
879
- }
455
+ /* eslint-enable camelcase */
880
456
 
881
- /*
882
- * Base64 url safe encoding of an array
883
- */
884
- #base64UrlSafeEncode(buf) {
885
- return this.#encode64(buf).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
886
- }
457
+ // eslint-disable-next-line class-methods-use-this
458
+ #sha256Hash(str) {
459
+ return window.crypto.subtle.digest("SHA-256", new TextEncoder().encode(str));
460
+ }
887
461
 
888
- /*
889
- * Get Random string starting with buffer of specified size
890
- */
891
- #getRandomString(nSize) {
892
- const buf = new Uint8Array(nSize);
893
- this.crypto.getRandomValues(buf);
894
- return this.#base64UrlSafeEncode(buf);
895
- }
462
+ // Base64 encode
463
+ // eslint-disable-next-line class-methods-use-this
464
+ #encode64(buff) {
465
+ return window.btoa(new Uint8Array(buff).reduce((s, b) => s + String.fromCharCode(b), ''));
466
+ }
896
467
 
897
- /*
898
- * Calc code verifier if necessary
899
- */
900
- async #getCodeChallenge(codeVerifier) {
901
- return this.#sha256Hash(codeVerifier)
902
- .then((hashed) => {
903
- return this.#base64UrlSafeEncode(hashed);
904
- })
905
- .catch((error) => {
906
- // eslint-disable-next-line no-console
907
- console.error(`Error calculation code challenge for PKCE: ${error}`);
908
- })
909
- .finally(() => {
910
- return null;
911
- });
912
- }
468
+ /*
469
+ * Base64 url safe encoding of an array
470
+ */
471
+ #base64UrlSafeEncode(buf) {
472
+ const s = this.#encode64(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
473
+ return s;
474
+ }
913
475
 
914
- /*
915
- * Return agent value for POST commands
916
- */
917
- #getAgent() {
918
- if (this.isNode && this.#config.ignoreInvalidCerts) {
919
- const options = { rejectUnauthorized: false };
920
- if (this.#config.legacyTLS) {
921
- options.secureOptions = this.crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT;
922
- }
923
- return new this.https.Agent(options);
924
- }
925
- return undefined;
926
- }
476
+ /* Calc code verifier if necessary
477
+ */
478
+ /* eslint-disable camelcase */
479
+ #getCodeChallenge(code_verifier) {
480
+ return this.#sha256Hash(code_verifier).then (
481
+ (hashed) => {
482
+ return this.#base64UrlSafeEncode(hashed)
483
+ }
484
+ ).catch(
485
+ (error) => {
486
+ // eslint-disable-next-line no-console
487
+ console.log(error)
488
+ }
489
+ ).finally(
490
+ () => { return null }
491
+ )
927
492
  }
493
+ /* eslint-enable camelcase */
494
+
495
+ }
928
496
 
929
- export default PegaAuth;
497
+ export default PegaAuth;