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