@pega/react-sdk-overrides 0.23.25 → 8.8.20
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.
- package/lib/designSystemExtension/AlertBanner/AlertBanner.tsx +47 -0
- package/lib/designSystemExtension/AlertBanner/index.tsx +1 -0
- package/lib/designSystemExtension/Banner/Banner.css +4 -0
- package/lib/designSystemExtension/Banner/Banner.tsx +25 -11
- package/lib/designSystemExtension/CaseSummaryFields/CaseSummaryFields.tsx +15 -10
- package/lib/designSystemExtension/DetailsFields/DetailsFields.tsx +10 -11
- package/lib/designSystemExtension/FieldGroup/FieldGroup.tsx +10 -3
- package/lib/designSystemExtension/FieldGroupList/FieldGroupList.tsx +11 -5
- package/lib/designSystemExtension/FieldValueList/FieldValueList.tsx +11 -3
- package/lib/designSystemExtension/Operator/Operator.tsx +30 -21
- package/lib/designSystemExtension/Pulse/Pulse.tsx +11 -7
- package/lib/designSystemExtension/RichTextEditor/RichTextEditor.tsx +121 -0
- package/lib/designSystemExtension/RichTextEditor/index.tsx +1 -0
- package/lib/designSystemExtension/WssQuickCreate/WssQuickCreate.css +6 -2
- package/lib/designSystemExtension/WssQuickCreate/WssQuickCreate.tsx +12 -3
- package/lib/field/AutoComplete/AutoComplete.tsx +37 -18
- package/lib/field/CancelAlert/CancelAlert.tsx +21 -12
- package/lib/field/Checkbox/Checkbox.tsx +41 -17
- package/lib/field/Currency/Currency.tsx +23 -15
- package/lib/field/Currency/currency-utils.ts +1 -2
- package/lib/field/Date/Date.tsx +31 -17
- package/lib/field/DateTime/DateTime.tsx +26 -15
- package/lib/field/Decimal/Decimal.tsx +82 -19
- package/lib/field/Dropdown/Dropdown.tsx +57 -14
- package/lib/field/Email/Email.tsx +17 -9
- package/lib/field/Integer/Integer.tsx +15 -7
- package/lib/field/Percentage/Percentage.tsx +15 -7
- package/lib/field/Phone/Phone.tsx +21 -12
- package/lib/field/RadioButtons/RadioButtons.tsx +54 -27
- package/lib/field/RichText/RichText.tsx +93 -0
- package/lib/field/RichText/index.tsx +1 -0
- package/lib/field/ScalarList/ScalarList.tsx +64 -0
- package/lib/field/ScalarList/config-ext.json +8 -0
- package/lib/field/ScalarList/index.tsx +1 -0
- package/lib/field/SemanticLink/SemanticLink.tsx +26 -25
- package/lib/field/SemanticLink/utils.ts +2 -1
- package/lib/field/TextArea/TextArea.tsx +14 -5
- package/lib/field/TextContent/TextContent.tsx +10 -1
- package/lib/field/TextInput/TextInput.tsx +40 -11
- package/lib/field/Time/Time.tsx +29 -26
- package/lib/field/URL/URL.tsx +24 -8
- package/lib/field/UserReference/UserReference.tsx +52 -60
- package/lib/helpers/{attachmentHelpers.js → attachmentHelpers.ts} +5 -5
- package/lib/helpers/auth.js +741 -390
- package/lib/helpers/authManager.ts +933 -0
- package/lib/helpers/case-utils.tsx +103 -0
- package/lib/helpers/common-utils.ts +4 -0
- package/lib/helpers/config_access.js +63 -145
- package/lib/helpers/data_page.ts +2 -1
- package/lib/helpers/date-format-utils.ts +29 -19
- package/lib/helpers/{event-utils.js → event-utils.ts} +1 -1
- package/lib/helpers/{field-group-utils.js → field-group-utils.ts} +4 -3
- package/lib/helpers/formatters/{Currency.js → Currency.ts} +13 -12
- package/lib/helpers/formatters/{CurrencyMap.js → CurrencyMap.ts} +8 -5
- package/lib/helpers/formatters/{Date.js → Date.ts} +2 -2
- package/lib/helpers/formatters/{common.js → common.ts} +4 -4
- package/lib/helpers/formatters/{index.js → index.ts} +3 -3
- package/lib/helpers/simpleTableHelpers.ts +10 -6
- package/lib/helpers/state-utils.tsx +47 -0
- package/lib/helpers/template-utils.ts +3 -4
- package/lib/helpers/utils.ts +12 -4
- package/lib/helpers/versionHelpers.ts +0 -1
- package/lib/infra/ActionButtons/ActionButtons.tsx +13 -18
- package/lib/infra/Assignment/Assignment.tsx +38 -32
- package/lib/infra/AssignmentCard/AssignmentCard.tsx +15 -19
- package/lib/infra/Containers/FlowContainer/FlowContainer.tsx +76 -64
- package/lib/infra/Containers/FlowContainer/{helpers.js → helpers.ts} +18 -16
- package/lib/infra/Containers/ModalViewContainer/ModalViewContainer.tsx +41 -27
- package/lib/infra/Containers/ViewContainer/ViewContainer.tsx +19 -28
- package/lib/infra/DashboardFilter/DashboardFilter.tsx +16 -20
- package/lib/infra/DashboardFilter/filterUtils.tsx +3 -1
- package/lib/infra/DeferLoad/DeferLoad.tsx +16 -19
- package/lib/infra/ErrorBoundary/ErrorBoundary.tsx +20 -19
- package/lib/infra/MultiStep/MultiStep.tsx +24 -24
- package/lib/infra/NavBar/NavBar.tsx +23 -24
- package/lib/infra/Reference/Reference.tsx +13 -18
- package/lib/infra/Region/Region.tsx +8 -6
- package/lib/infra/RootContainer/RootContainer.tsx +32 -39
- package/lib/infra/Stages/Stages.tsx +15 -9
- package/lib/infra/VerticalTabs/LeftAlignVerticalTabs/LeftAlignVerticalTabs.tsx +8 -1
- package/lib/infra/VerticalTabs/VerticalTabs/VerticalTabs.tsx +12 -12
- package/lib/infra/View/View.tsx +30 -58
- package/lib/template/AppShell/AppShell.tsx +51 -34
- package/lib/template/BannerPage/BannerPage.tsx +26 -31
- package/lib/template/CaseSummary/CaseSummary.tsx +15 -8
- package/lib/template/CaseView/CaseView.tsx +137 -100
- package/lib/template/CaseViewActionsMenu/CaseViewActionsMenu.tsx +27 -27
- package/lib/template/Confirmation/Confirmation.tsx +29 -52
- package/lib/template/DataReference/DataReference.tsx +50 -52
- package/lib/template/DefaultForm/DefaultForm.tsx +29 -20
- package/lib/template/DefaultForm/utils/index.ts +33 -0
- package/lib/template/Details/Details/Details.tsx +16 -17
- package/lib/template/Details/DetailsSubTabs/DetailsSubTabs.tsx +13 -16
- package/lib/template/Details/DetailsThreeColumn/DetailsThreeColumn.tsx +19 -18
- package/lib/template/Details/DetailsTwoColumn/DetailsTwoColumn.tsx +20 -18
- package/lib/template/FieldGroupTemplate/FieldGroupTemplate.tsx +24 -27
- package/lib/template/InlineDashboard/InlineDashboard.tsx +11 -7
- package/lib/template/InlineDashboardPage/InlineDashboardPage.tsx +19 -17
- package/lib/template/ListPage/ListPage.tsx +14 -13
- package/lib/template/ListView/ListView.tsx +140 -152
- package/lib/template/ListView/{hooks.js → hooks.ts} +3 -1
- package/lib/template/ListView/{utils.js → utils.ts} +172 -23
- package/lib/template/MultiReferenceReadOnly/MultiReferenceReadOnly.tsx +12 -17
- package/lib/template/NarrowWide/NarrowWide/NarrowWide.tsx +16 -1
- package/lib/template/NarrowWide/NarrowWideDetails/NarrowWideDetails.tsx +19 -18
- package/lib/template/NarrowWide/NarrowWideForm/NarrowWideForm.tsx +9 -1
- package/lib/template/NarrowWide/NarrowWidePage/NarrowWidePage.tsx +17 -17
- package/lib/template/OneColumn/OneColumn/OneColumn.tsx +8 -7
- package/lib/template/OneColumn/OneColumnPage/OneColumnPage.tsx +10 -10
- package/lib/template/OneColumn/OneColumnTab/OneColumnTab.tsx +5 -7
- package/lib/template/PromotedFilters/PromotedFilters.tsx +23 -17
- package/lib/template/SimpleTable/SimpleTable/SimpleTable.tsx +103 -6
- package/lib/template/SimpleTable/SimpleTableManual/SimpleTableManual.tsx +28 -7
- package/lib/template/SimpleTable/SimpleTableSelect/SimpleTableSelect.tsx +26 -31
- package/lib/template/SingleReferenceReadOnly/SingleReferenceReadOnly.tsx +34 -36
- package/lib/template/SubTabs/SubTabs.tsx +10 -11
- package/lib/template/SubTabs/tabUtils.ts +0 -2
- package/lib/template/TwoColumn/TwoColumn/TwoColumn.tsx +10 -15
- package/lib/template/TwoColumn/TwoColumnPage/TwoColumnPage.tsx +10 -10
- package/lib/template/TwoColumn/TwoColumnTab/TwoColumnTab.tsx +10 -12
- package/lib/template/WideNarrow/WideNarrow/WideNarrow.tsx +17 -3
- package/lib/template/WideNarrow/WideNarrowDetails/WideNarrowDetails.tsx +35 -25
- package/lib/template/WideNarrow/WideNarrowForm/WideNarrowForm.tsx +7 -1
- package/lib/template/WideNarrow/WideNarrowPage/WideNarrowPage.tsx +15 -17
- package/lib/template/WssNavBar/WssNavBar.tsx +36 -14
- package/lib/widget/AppAnnouncement/AppAnnouncement.tsx +13 -21
- package/lib/widget/Attachment/Attachment.css +15 -3
- package/lib/widget/Attachment/Attachment.tsx +51 -32
- package/lib/widget/CaseHistory/CaseHistory.tsx +13 -11
- package/lib/widget/FileUtility/ActionButtonsForFileUtil/ActionButtonsForFileUtil.tsx +13 -1
- package/lib/widget/FileUtility/FileUtility/FileUtility.tsx +40 -26
- package/lib/widget/Followers/Followers.tsx +10 -11
- package/lib/widget/QuickCreate/QuickCreate.tsx +15 -6
- package/lib/widget/SummaryItem/SummaryItem.tsx +12 -4
- package/lib/widget/SummaryList/SummaryList.tsx +17 -3
- package/lib/widget/ToDo/ToDo.tsx +69 -104
- package/package.json +1 -1
- package/lib/helpers/authManager.js +0 -631
- /package/lib/helpers/formatters/{Boolean.js → Boolean.ts} +0 -0
- /package/lib/helpers/{reactContextHelpers.js → reactContextHelpers.ts} +0 -0
- /package/lib/template/ListView/{DefaultViewMeta.js → DefaultViewMeta.ts} +0 -0
package/lib/helpers/auth.js
CHANGED
|
@@ -1,483 +1,834 @@
|
|
|
1
1
|
class PegaAuth {
|
|
2
|
-
|
|
3
|
-
|
|
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') {
|
|
4
13
|
this.ssKeyConfig = ssKeyConfig;
|
|
5
|
-
this.
|
|
6
|
-
this
|
|
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';
|
|
23
|
+
|
|
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
|
+
}
|
|
7
36
|
}
|
|
8
37
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
38
|
+
#reloadSS(ssKey) {
|
|
39
|
+
const sItem = window.sessionStorage.getItem(ssKey);
|
|
40
|
+
let obj = {};
|
|
41
|
+
if (sItem) {
|
|
42
|
+
try {
|
|
43
|
+
obj = JSON.parse(sItem);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
try {
|
|
46
|
+
obj = JSON.parse(atob(sItem));
|
|
47
|
+
} catch (err) {
|
|
48
|
+
obj = {};
|
|
49
|
+
}
|
|
22
50
|
}
|
|
51
|
+
}
|
|
52
|
+
if (ssKey === this.ssKeyConfig) {
|
|
53
|
+
this.#config = sItem ? obj : {};
|
|
54
|
+
} else {
|
|
55
|
+
this.#dynState = sItem ? obj : {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
23
58
|
|
|
24
|
-
|
|
59
|
+
#reloadConfig() {
|
|
60
|
+
if (this.ssKeyConfig) {
|
|
61
|
+
this. #reloadSS(this.ssKeyConfig);
|
|
62
|
+
}
|
|
63
|
+
if (this.ssKeyDynState) {
|
|
64
|
+
this.#reloadSS(this.ssKeyDynState);
|
|
65
|
+
}
|
|
25
66
|
}
|
|
26
67
|
|
|
27
68
|
#updateConfig() {
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
}
|
|
30
84
|
}
|
|
31
85
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
+
}
|
|
36
105
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
this.#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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;
|
|
53
123
|
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
54
126
|
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
}
|
|
57
152
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
(userIdentifier ? `&UserIdentifier=${encodeURIComponent(userIdentifier)}` : '') +
|
|
64
|
-
(userIdentifier && password ? `&Password=${encodeURIComponent(window.atob(password))}` : '');
|
|
153
|
+
// If sessionIndex exists then increment attempts count (we will stop sending session_index after two failures)
|
|
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;
|
|
157
|
+
}
|
|
65
158
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
+
|
|
167
|
+
// Persist codeVerifier in session storage so it survives the redirects that are to follow
|
|
168
|
+
this.#updateConfig();
|
|
169
|
+
|
|
170
|
+
// Trim alias to include just the real alias piece
|
|
171
|
+
const addtlScope = appAlias ? `+app.alias.${appAlias.replace(/^app\//, '')}` : '';
|
|
172
|
+
const scope = bInfinity ? `openid${addtlScope}` : 'user_info';
|
|
173
|
+
|
|
174
|
+
// Add explicit creds if specified to try to avoid login popup
|
|
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}`;
|
|
70
192
|
}
|
|
71
193
|
|
|
72
194
|
async login() {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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() {
|
|
214
|
+
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;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const redirectOrigin = fnGetRedirectUriOrigin();
|
|
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
|
+
}
|
|
78
308
|
};
|
|
79
309
|
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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();
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if( this.config.iframeLoginUI ) {
|
|
210
|
-
elIframe.style.display="block";
|
|
211
|
-
elCloseBtn.style.display="block";
|
|
212
|
-
} else {
|
|
213
|
-
fnCloseIframe();
|
|
214
|
-
fnOpenPopup();
|
|
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 {
|
|
409
|
+
fnCloseIframe();
|
|
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)
|
|
215
427
|
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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();
|
|
462
|
+
|
|
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;
|
|
490
|
+
} else {
|
|
491
|
+
fnSetSilentAuthFailed(true);
|
|
492
|
+
bDisablePromptNone = false;
|
|
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
|
+
};
|
|
515
|
+
|
|
516
|
+
this.#buildAuthorizeUrl(state).then((url) => {
|
|
517
|
+
theUrl = url;
|
|
518
|
+
doAuthorize();
|
|
258
519
|
});
|
|
520
|
+
});
|
|
259
521
|
}
|
|
260
522
|
|
|
261
523
|
// Login redirect
|
|
262
524
|
loginRedirect() {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
525
|
+
// eslint-disable-next-line no-restricted-globals
|
|
526
|
+
const state = btoa(location.origin);
|
|
527
|
+
this.#buildAuthorizeUrl(state).then((url) => {
|
|
528
|
+
// eslint-disable-next-line no-restricted-globals
|
|
529
|
+
location.href = url;
|
|
530
|
+
});
|
|
269
531
|
}
|
|
270
532
|
|
|
533
|
+
// check state
|
|
534
|
+
checkStateMatch(state) {
|
|
535
|
+
return state === this.#dynState.state;
|
|
536
|
+
}
|
|
537
|
+
|
|
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
|
+
}
|
|
271
548
|
|
|
272
549
|
// For PKCE token endpoint includes code_verifier
|
|
273
550
|
getToken(authCode) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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;
|
|
281
576
|
const urlParams = new URLSearchParams(queryString);
|
|
282
|
-
|
|
577
|
+
authCode = urlParams.get('code');
|
|
578
|
+
}
|
|
283
579
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
580
|
+
const formData = new URLSearchParams();
|
|
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);
|
|
288
602
|
}
|
|
289
|
-
|
|
290
|
-
formData.append(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return fetch(tokenUri, {
|
|
621
|
+
agent: this.#getAgent(),
|
|
622
|
+
method: 'POST',
|
|
623
|
+
headers: new Headers({
|
|
624
|
+
'content-type': this.urlencoded
|
|
625
|
+
}),
|
|
626
|
+
|
|
627
|
+
body: formData.toString()
|
|
628
|
+
})
|
|
302
629
|
.then((response) => response.json())
|
|
303
|
-
.then(token => {
|
|
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 {
|
|
304
635
|
// .expires_in contains the # of seconds before access token expires
|
|
305
636
|
// add property to keep track of current time when the token expires
|
|
306
|
-
token.eA = Date.now() +
|
|
307
|
-
|
|
308
|
-
|
|
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;
|
|
309
647
|
}
|
|
310
648
|
// If there is a session_index then move this to the peConfig structure (as used on authorize)
|
|
311
|
-
if(
|
|
312
|
-
|
|
649
|
+
if (token.session_index) {
|
|
650
|
+
this.#dynState.sessionIndex = token.session_index;
|
|
313
651
|
}
|
|
314
652
|
// If we got a token and have a session index, then reset the sessionIndexAttempts
|
|
315
|
-
if(
|
|
316
|
-
this.
|
|
653
|
+
if (this.#dynState.sessionIndex) {
|
|
654
|
+
this.#dynState.sessionIndexAttempts = 0;
|
|
317
655
|
}
|
|
318
656
|
this.#updateConfig();
|
|
319
|
-
|
|
657
|
+
}
|
|
658
|
+
return token;
|
|
320
659
|
})
|
|
321
|
-
.catch(e => {
|
|
660
|
+
.catch((e) => {
|
|
322
661
|
// eslint-disable-next-line no-console
|
|
323
|
-
console.
|
|
662
|
+
console.error(`Token endpoint error: ${e}`);
|
|
324
663
|
});
|
|
325
664
|
}
|
|
326
665
|
|
|
327
666
|
/* eslint-disable camelcase */
|
|
328
667
|
async refreshToken(refresh_token) {
|
|
329
|
-
|
|
668
|
+
const { clientId, clientSecret, tokenUri } = this.#config;
|
|
330
669
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
formData.append("grant_type", "refresh_token");
|
|
337
|
-
formData.append("refresh_token", refresh_token);
|
|
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
|
+
}
|
|
338
674
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
"content-type": "application/x-www-form-urlencoded",
|
|
343
|
-
}),
|
|
675
|
+
if (!refresh_token) {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
344
678
|
|
|
345
|
-
|
|
346
|
-
|
|
679
|
+
const formData = new URLSearchParams();
|
|
680
|
+
formData.append('client_id', clientId);
|
|
681
|
+
if (clientSecret) {
|
|
682
|
+
formData.append('client_secret', clientSecret);
|
|
683
|
+
}
|
|
684
|
+
formData.append('grant_type', 'refresh_token');
|
|
685
|
+
formData.append('refresh_token', refresh_token);
|
|
686
|
+
|
|
687
|
+
return fetch(tokenUri, {
|
|
688
|
+
agent: this.#getAgent(),
|
|
689
|
+
method: 'POST',
|
|
690
|
+
headers: new Headers({
|
|
691
|
+
'content-type': this.urlencoded
|
|
692
|
+
}),
|
|
693
|
+
|
|
694
|
+
body: formData.toString()
|
|
695
|
+
})
|
|
347
696
|
.then((response) => {
|
|
348
|
-
if(
|
|
349
|
-
|
|
697
|
+
if (!response.ok && response.status === 401) {
|
|
698
|
+
return null;
|
|
350
699
|
}
|
|
351
700
|
return response.json();
|
|
352
701
|
})
|
|
353
|
-
.then(token => {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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;
|
|
707
|
+
}
|
|
708
|
+
return token;
|
|
360
709
|
})
|
|
361
|
-
.catch(e => {
|
|
710
|
+
.catch((e) => {
|
|
362
711
|
// eslint-disable-next-line no-console
|
|
363
|
-
console.
|
|
712
|
+
console.warn(`Refresh token failed: ${e}`);
|
|
713
|
+
return null;
|
|
364
714
|
});
|
|
365
715
|
}
|
|
366
716
|
|
|
367
717
|
async revokeTokens(access_token, refresh_token = null) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
718
|
+
if (Object.keys(this.#config).length === 0) {
|
|
719
|
+
// Must have a config structure to proceed
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const { clientId, clientSecret, revokeUri } = this.#config;
|
|
373
723
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
const aTknProps = ["access_token"];
|
|
380
|
-
if( refresh_token ) {
|
|
381
|
-
aTknProps.push("refresh_token");
|
|
382
|
-
}
|
|
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
|
-
})
|
|
395
|
-
.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 => {
|
|
402
|
-
// eslint-disable-next-line no-console
|
|
403
|
-
console.log(e);
|
|
404
|
-
});
|
|
405
|
-
} );
|
|
406
|
-
// Also clobber any sessionIndex
|
|
407
|
-
if( this.config.sessionIndex ) {
|
|
408
|
-
delete this.config.sessionIndex;
|
|
409
|
-
this.#updateConfig();
|
|
410
|
-
}
|
|
411
|
-
}
|
|
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();
|
|
727
|
+
}
|
|
412
728
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
729
|
+
const hdrs = { 'content-type': this.urlencoded };
|
|
730
|
+
if (clientSecret) {
|
|
731
|
+
const basicCreds = btoa(`${clientId}:${clientSecret}`);
|
|
732
|
+
hdrs.authorization = `Basic ${basicCreds}`;
|
|
733
|
+
}
|
|
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
|
+
})
|
|
751
|
+
.then((response) => {
|
|
752
|
+
if (!response.ok) {
|
|
428
753
|
// eslint-disable-next-line no-console
|
|
429
|
-
console.
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
// eslint-disable-next-line no-console
|
|
437
|
-
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}`);
|
|
760
|
+
});
|
|
438
761
|
});
|
|
762
|
+
this.#config.silentAuthFailed = false;
|
|
763
|
+
// Also clobber any sessionIndex
|
|
764
|
+
this.#updateSessionIndex(null);
|
|
439
765
|
}
|
|
440
|
-
|
|
441
766
|
/* eslint-enable camelcase */
|
|
442
767
|
|
|
443
|
-
// eslint-disable-next-line class-methods-use-this
|
|
444
768
|
#sha256Hash(str) {
|
|
445
|
-
|
|
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
|
+
});
|
|
775
|
+
}
|
|
776
|
+
return this.subtle.digest('SHA-256', new TextEncoder().encode(str));
|
|
446
777
|
}
|
|
447
778
|
|
|
448
779
|
// Base64 encode
|
|
449
|
-
|
|
780
|
+
/* eslint-disable-next-line class-methods-use-this */
|
|
450
781
|
#encode64(buff) {
|
|
451
|
-
|
|
782
|
+
return btoa(new Uint8Array(buff).reduce((s, b) => s + String.fromCharCode(b), ''));
|
|
452
783
|
}
|
|
453
784
|
|
|
454
785
|
/*
|
|
455
786
|
* Base64 url safe encoding of an array
|
|
456
787
|
*/
|
|
457
788
|
#base64UrlSafeEncode(buf) {
|
|
458
|
-
|
|
459
|
-
|
|
789
|
+
return this.#encode64(buf).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
790
|
+
}
|
|
791
|
+
|
|
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);
|
|
460
799
|
}
|
|
461
800
|
|
|
462
801
|
/* Calc code verifier if necessary
|
|
463
802
|
*/
|
|
464
803
|
/* eslint-disable camelcase */
|
|
465
|
-
#getCodeChallenge(code_verifier) {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
)
|
|
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
|
+
});
|
|
478
816
|
}
|
|
479
817
|
/* eslint-enable camelcase */
|
|
480
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
|
+
}
|
|
481
832
|
}
|
|
482
833
|
|
|
483
834
|
export default PegaAuth;
|