@pega/auth 0.1.0

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