@pega/react-sdk-overrides 8.23.10 → 23.1.10

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