oidc-spa 8.4.8 → 8.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +2 -5
  2. package/core/createOidc.js +3 -1
  3. package/core/createOidc.js.map +1 -1
  4. package/core/earlyInit.d.ts +45 -7
  5. package/core/earlyInit.js +69 -153
  6. package/core/earlyInit.js.map +1 -1
  7. package/core/oidcClientTsUserToTokens.d.ts +1 -0
  8. package/core/oidcClientTsUserToTokens.js +11 -1
  9. package/core/oidcClientTsUserToTokens.js.map +1 -1
  10. package/core/tokenExfiltrationDefense.d.ts +6 -0
  11. package/core/tokenExfiltrationDefense.js +616 -0
  12. package/core/tokenExfiltrationDefense.js.map +1 -0
  13. package/core/tokenExfiltrationDefense_legacy.d.ts +8 -0
  14. package/core/tokenExfiltrationDefense_legacy.js +133 -0
  15. package/core/tokenExfiltrationDefense_legacy.js.map +1 -0
  16. package/core/tokenPlaceholderSubstitution.d.ts +13 -0
  17. package/core/tokenPlaceholderSubstitution.js +79 -0
  18. package/core/tokenPlaceholderSubstitution.js.map +1 -0
  19. package/esm/core/createOidc.js +3 -1
  20. package/esm/core/createOidc.js.map +1 -1
  21. package/esm/core/earlyInit.d.ts +45 -7
  22. package/esm/core/earlyInit.js +69 -153
  23. package/esm/core/earlyInit.js.map +1 -1
  24. package/esm/core/oidcClientTsUserToTokens.d.ts +1 -0
  25. package/esm/core/oidcClientTsUserToTokens.js +11 -1
  26. package/esm/core/oidcClientTsUserToTokens.js.map +1 -1
  27. package/esm/core/tokenExfiltrationDefense.d.ts +6 -0
  28. package/esm/core/tokenExfiltrationDefense.js +613 -0
  29. package/esm/core/tokenExfiltrationDefense.js.map +1 -0
  30. package/esm/core/tokenExfiltrationDefense_legacy.d.ts +8 -0
  31. package/esm/core/tokenExfiltrationDefense_legacy.js +130 -0
  32. package/esm/core/tokenExfiltrationDefense_legacy.js.map +1 -0
  33. package/esm/core/tokenPlaceholderSubstitution.d.ts +13 -0
  34. package/esm/core/tokenPlaceholderSubstitution.js +73 -0
  35. package/esm/core/tokenPlaceholderSubstitution.js.map +1 -0
  36. package/esm/tools/isDomain.d.ts +1 -0
  37. package/esm/tools/isDomain.js +16 -0
  38. package/esm/tools/isDomain.js.map +1 -0
  39. package/esm/tools/isHostnameAuthorized.d.ts +5 -0
  40. package/esm/tools/isHostnameAuthorized.js +74 -0
  41. package/esm/tools/isHostnameAuthorized.js.map +1 -0
  42. package/esm/tools/isLikelyDevServer.js +18 -10
  43. package/esm/tools/isLikelyDevServer.js.map +1 -1
  44. package/package.json +1 -1
  45. package/src/core/createOidc.ts +2 -0
  46. package/src/core/earlyInit.ts +138 -192
  47. package/src/core/oidcClientTsUserToTokens.ts +14 -0
  48. package/src/core/tokenExfiltrationDefense.ts +874 -0
  49. package/src/core/tokenExfiltrationDefense_legacy.ts +165 -0
  50. package/src/core/tokenPlaceholderSubstitution.ts +105 -0
  51. package/src/tools/isDomain.ts +18 -0
  52. package/src/tools/isHostnameAuthorized.ts +91 -0
  53. package/src/tools/isLikelyDevServer.ts +23 -11
  54. package/src/vite-plugin/handleClientEntrypoint.ts +57 -20
  55. package/src/vite-plugin/vite-plugin.ts +5 -10
  56. package/tools/isDomain.d.ts +1 -0
  57. package/tools/isDomain.js +19 -0
  58. package/tools/isDomain.js.map +1 -0
  59. package/tools/isHostnameAuthorized.d.ts +5 -0
  60. package/tools/isHostnameAuthorized.js +77 -0
  61. package/tools/isHostnameAuthorized.js.map +1 -0
  62. package/tools/isLikelyDevServer.js +18 -10
  63. package/tools/isLikelyDevServer.js.map +1 -1
  64. package/vite-plugin/handleClientEntrypoint.js +36 -17
  65. package/vite-plugin/handleClientEntrypoint.js.map +1 -1
  66. package/vite-plugin/vite-plugin.d.ts +3 -4
  67. package/vite-plugin/vite-plugin.js +1 -5
  68. package/vite-plugin/vite-plugin.js.map +1 -1
@@ -0,0 +1,874 @@
1
+ import { assert } from "../tools/tsafe/assert";
2
+ import {
3
+ markTokenSubstitutionAdEnabled,
4
+ substitutePlaceholderByRealToken
5
+ } from "./tokenPlaceholderSubstitution";
6
+ import { getIsHostnameAuthorized } from "../tools/isHostnameAuthorized";
7
+
8
+ type Params = {
9
+ resourceServersAllowedHostnames: string[] | undefined;
10
+ serviceWorkersAllowedHostnames: string[] | undefined;
11
+ };
12
+
13
+ const viteHashedJsAssetPathRegExp = /\/assets\/[^/]+-[a-zA-Z0-9_-]{8}\.js$/;
14
+
15
+ export function enableTokenExfiltrationDefense(params: Params) {
16
+ const { resourceServersAllowedHostnames = [], serviceWorkersAllowedHostnames = [] } = params;
17
+
18
+ markTokenSubstitutionAdEnabled();
19
+
20
+ patchFetchApiToSubstituteTokenPlaceholder({ resourceServersAllowedHostnames });
21
+ patchXMLHttpRequestApiToSubstituteTokenPlaceholder({ resourceServersAllowedHostnames });
22
+ patchWebSocketApiToSubstituteTokenPlaceholder({ resourceServersAllowedHostnames });
23
+ patchEventSourceApiToSubstituteTokenPlaceholder({ resourceServersAllowedHostnames });
24
+ patchNavigatorSendBeaconApiToSubstituteTokenPlaceholder({ resourceServersAllowedHostnames });
25
+ restrictServiceWorkerRegistration({ serviceWorkersAllowedHostnames });
26
+
27
+ runMonkeyPatchingPrevention();
28
+ }
29
+
30
+ function patchFetchApiToSubstituteTokenPlaceholder(params: {
31
+ resourceServersAllowedHostnames: string[];
32
+ }) {
33
+ const { resourceServersAllowedHostnames } = params;
34
+
35
+ const fetch_actual = window.fetch;
36
+
37
+ window.fetch = async function fetch(input, init) {
38
+ const request = input instanceof Request ? input : new Request(input, init);
39
+
40
+ prevent_fetching_of_hashed_js_assets: {
41
+ const { pathname } = new URL(request.url, window.location.href);
42
+
43
+ if (!viteHashedJsAssetPathRegExp.test(pathname)) {
44
+ break prevent_fetching_of_hashed_js_assets;
45
+ }
46
+
47
+ throw new Error("oidc-spa: Blocked request to hashed js static asset.");
48
+ }
49
+
50
+ let didSubstitute = false;
51
+
52
+ const headers = new Headers();
53
+ request.headers.forEach((value, key) => {
54
+ const nextValue = substitutePlaceholderByRealToken(value);
55
+
56
+ if (nextValue !== value) {
57
+ didSubstitute = true;
58
+ }
59
+
60
+ headers.set(key, nextValue);
61
+ });
62
+
63
+ let body: BodyInit | undefined;
64
+
65
+ handle_body: {
66
+ from_init: {
67
+ if (!init) {
68
+ break from_init;
69
+ }
70
+
71
+ if (!init.body) {
72
+ break from_init;
73
+ }
74
+
75
+ if (input instanceof Request && input.body !== null) {
76
+ break from_init;
77
+ }
78
+
79
+ if (typeof init.body === "string") {
80
+ body = substitutePlaceholderByRealToken(init.body);
81
+
82
+ if (init.body !== body) {
83
+ didSubstitute = true;
84
+ }
85
+
86
+ break handle_body;
87
+ }
88
+
89
+ if (init.body instanceof URLSearchParams) {
90
+ let didUrlSearchParamsSubstitute = false;
91
+ const next = new URLSearchParams();
92
+
93
+ init.body.forEach((value, key) => {
94
+ const nextValue = substitutePlaceholderByRealToken(value);
95
+
96
+ if (nextValue !== value) {
97
+ didUrlSearchParamsSubstitute = true;
98
+ }
99
+
100
+ next.append(key, nextValue);
101
+ });
102
+
103
+ if (didUrlSearchParamsSubstitute) {
104
+ didSubstitute = true;
105
+ }
106
+
107
+ body = didUrlSearchParamsSubstitute ? next : init.body;
108
+
109
+ break handle_body;
110
+ }
111
+
112
+ if (init.body instanceof FormData) {
113
+ let didFormDataSubstitute = false;
114
+ const next = new FormData();
115
+
116
+ init.body.forEach((value, key) => {
117
+ if (typeof value === "string") {
118
+ const nextValue = substitutePlaceholderByRealToken(value);
119
+
120
+ if (nextValue !== value) {
121
+ didFormDataSubstitute = true;
122
+ }
123
+
124
+ next.append(key, nextValue);
125
+
126
+ return;
127
+ }
128
+
129
+ next.append(key, value);
130
+ });
131
+
132
+ if (didFormDataSubstitute) {
133
+ didSubstitute = true;
134
+ }
135
+
136
+ body = didFormDataSubstitute ? next : init.body;
137
+
138
+ break handle_body;
139
+ }
140
+
141
+ if (init.body instanceof Blob) {
142
+ break from_init;
143
+ }
144
+
145
+ body = init.body;
146
+ break handle_body;
147
+ }
148
+
149
+ if (request.body === null) {
150
+ body = undefined;
151
+ break handle_body;
152
+ }
153
+
154
+ const shouldInspectBody = (() => {
155
+ let ct = request.headers.get("Content-Type");
156
+
157
+ if (ct === null) {
158
+ return false;
159
+ }
160
+
161
+ ct = ct.toLocaleLowerCase();
162
+
163
+ if (
164
+ !ct.startsWith("application/json") &&
165
+ !ct.startsWith("application/x-www-form-urlencoded")
166
+ ) {
167
+ return false;
168
+ }
169
+
170
+ const len_str = request.headers.get("Content-Length");
171
+
172
+ if (!len_str) {
173
+ return false;
174
+ }
175
+
176
+ const len = parseInt(len_str, 10);
177
+
178
+ if (!Number.isFinite(len) || len > 100_000) {
179
+ return false;
180
+ }
181
+
182
+ return true;
183
+ })();
184
+
185
+ if (!shouldInspectBody) {
186
+ body = request.body;
187
+ break handle_body;
188
+ }
189
+
190
+ const bodyText = await request.clone().text();
191
+ const nextBodyText = substitutePlaceholderByRealToken(bodyText);
192
+
193
+ if (nextBodyText !== bodyText) {
194
+ didSubstitute = true;
195
+ }
196
+
197
+ body = nextBodyText;
198
+ }
199
+
200
+ block_authed_request_to_unauthorized_hostnames: {
201
+ if (!didSubstitute) {
202
+ break block_authed_request_to_unauthorized_hostnames;
203
+ }
204
+
205
+ const { hostname } = new URL(request.url, window.location.href);
206
+
207
+ if (
208
+ getIsHostnameAuthorized({
209
+ allowedHostnames: resourceServersAllowedHostnames,
210
+ extendAuthorizationToParentDomain: true,
211
+ hostname
212
+ })
213
+ ) {
214
+ break block_authed_request_to_unauthorized_hostnames;
215
+ }
216
+
217
+ throw new Error(
218
+ [
219
+ `oidc-spa: Blocked authed request to ${hostname}.`,
220
+ `To authorize this request add "${hostname}" to`,
221
+ "`resourceServersAllowedHostnames`."
222
+ ].join(" ")
223
+ );
224
+ }
225
+
226
+ const nextInit: RequestInit = {
227
+ method: request.method,
228
+ headers,
229
+ body,
230
+ mode: request.mode,
231
+ credentials: request.credentials,
232
+ cache: request.cache,
233
+ redirect: request.redirect,
234
+ referrer: request.referrer,
235
+ referrerPolicy: request.referrerPolicy,
236
+ integrity: request.integrity,
237
+ keepalive: request.keepalive,
238
+ signal: request.signal
239
+ };
240
+
241
+ {
242
+ //@ts-expect-error
243
+ const duplex = init?.duplex ?? (input instanceof Request ? input.duplex : undefined);
244
+
245
+ if (duplex !== undefined) {
246
+ //@ts-expect-error
247
+ nextInit.duplex = duplex;
248
+ }
249
+ }
250
+
251
+ return fetch_actual(request.url, nextInit);
252
+ };
253
+ }
254
+
255
+ function patchXMLHttpRequestApiToSubstituteTokenPlaceholder(params: {
256
+ resourceServersAllowedHostnames: string[];
257
+ }) {
258
+ const { resourceServersAllowedHostnames } = params;
259
+
260
+ const open_actual = XMLHttpRequest.prototype.open;
261
+ const send_actual = XMLHttpRequest.prototype.send;
262
+ const setRequestHeader_actual = XMLHttpRequest.prototype.setRequestHeader;
263
+
264
+ type XhrData = {
265
+ url: string;
266
+ didSubstitute: boolean;
267
+ };
268
+
269
+ const xhrDataSymbol = Symbol("oidc-spa XMLHttpRequest data");
270
+
271
+ const getXhrData = (xhr: XMLHttpRequest): XhrData => {
272
+ const xhr_any = xhr as any;
273
+
274
+ if (xhr_any[xhrDataSymbol] !== undefined) {
275
+ return xhr_any[xhrDataSymbol];
276
+ }
277
+
278
+ const data: XhrData = {
279
+ url: "",
280
+ didSubstitute: false
281
+ };
282
+
283
+ xhr_any[xhrDataSymbol] = data;
284
+
285
+ return data;
286
+ };
287
+
288
+ XMLHttpRequest.prototype.open = function open(
289
+ method: string,
290
+ url: string | URL,
291
+ async?: boolean,
292
+ username?: string | null,
293
+ password?: string | null
294
+ ) {
295
+ const xhrData = getXhrData(this);
296
+
297
+ xhrData.url = typeof url === "string" ? url : url.href;
298
+ xhrData.didSubstitute = false;
299
+
300
+ if (async === undefined) {
301
+ return open_actual.bind(this)(method, url);
302
+ } else {
303
+ return open_actual.call(this, method, url, async, username, password);
304
+ }
305
+ };
306
+
307
+ XMLHttpRequest.prototype.setRequestHeader = function setRequestHeader(name, value) {
308
+ const xhrData = getXhrData(this);
309
+ const nextValue = substitutePlaceholderByRealToken(value);
310
+
311
+ if (nextValue !== value) {
312
+ xhrData.didSubstitute = true;
313
+ }
314
+
315
+ return setRequestHeader_actual.call(this, name, nextValue);
316
+ };
317
+
318
+ XMLHttpRequest.prototype.send = function send(body) {
319
+ const xhrData = getXhrData(this);
320
+
321
+ prevent_fetching_of_hashed_js_assets: {
322
+ const { pathname } = new URL(xhrData.url, window.location.href);
323
+
324
+ if (!viteHashedJsAssetPathRegExp.test(pathname)) {
325
+ break prevent_fetching_of_hashed_js_assets;
326
+ }
327
+
328
+ throw new Error("oidc-spa: Blocked request to hashed static asset.");
329
+ }
330
+
331
+ let nextBody = body;
332
+
333
+ if (typeof body === "string") {
334
+ const nextBodyText = substitutePlaceholderByRealToken(body);
335
+
336
+ if (nextBodyText !== body) {
337
+ xhrData.didSubstitute = true;
338
+ }
339
+
340
+ nextBody = nextBodyText;
341
+ }
342
+
343
+ block_authed_request_to_unauthorized_hostnames: {
344
+ if (!xhrData.didSubstitute) {
345
+ break block_authed_request_to_unauthorized_hostnames;
346
+ }
347
+
348
+ const { hostname } = new URL(xhrData.url, window.location.href);
349
+
350
+ if (
351
+ getIsHostnameAuthorized({
352
+ allowedHostnames: resourceServersAllowedHostnames,
353
+ extendAuthorizationToParentDomain: true,
354
+ hostname
355
+ })
356
+ ) {
357
+ break block_authed_request_to_unauthorized_hostnames;
358
+ }
359
+
360
+ throw new Error(
361
+ [
362
+ `oidc-spa: Blocked authed request to ${hostname}.`,
363
+ `To authorize this request add "${hostname}" to`,
364
+ "`resourceServersAllowedHostnames`."
365
+ ].join(" ")
366
+ );
367
+ }
368
+
369
+ return send_actual.call(this, nextBody as Parameters<XMLHttpRequest["send"]>[0]);
370
+ };
371
+ }
372
+
373
+ function patchWebSocketApiToSubstituteTokenPlaceholder(params: {
374
+ resourceServersAllowedHostnames: string[];
375
+ }) {
376
+ const { resourceServersAllowedHostnames } = params;
377
+
378
+ const WebSocket_actual = window.WebSocket;
379
+ const send_actual = WebSocket_actual.prototype.send;
380
+
381
+ type WsData = {
382
+ url: string;
383
+ hostname: string;
384
+ pathname: string;
385
+ didSubstitute: boolean;
386
+ };
387
+
388
+ const wsDataByWs = new WeakMap<WebSocket, WsData>();
389
+
390
+ const WebSocketPatched = function WebSocket(url: string | URL, protocols?: string | string[]) {
391
+ const urlStr = typeof url === "string" ? url : url.href;
392
+ const nextUrl = substitutePlaceholderByRealToken(urlStr);
393
+ let didSubstitute = nextUrl !== urlStr;
394
+
395
+ const nextProtocols = (() => {
396
+ if (protocols === undefined) {
397
+ return protocols;
398
+ }
399
+
400
+ if (typeof protocols === "string") {
401
+ const next = substitutePlaceholderByRealToken(protocols);
402
+
403
+ if (next !== protocols) {
404
+ didSubstitute = true;
405
+ }
406
+
407
+ return next;
408
+ }
409
+
410
+ let didProtocolsSubstitute = false;
411
+
412
+ const next = protocols.map(protocol => {
413
+ const nextProtocol = substitutePlaceholderByRealToken(protocol);
414
+
415
+ if (nextProtocol !== protocol) {
416
+ didProtocolsSubstitute = true;
417
+ }
418
+
419
+ return nextProtocol;
420
+ });
421
+
422
+ if (didProtocolsSubstitute) {
423
+ didSubstitute = true;
424
+ }
425
+
426
+ return next;
427
+ })();
428
+
429
+ const { hostname, pathname } = new URL(nextUrl, window.location.href);
430
+
431
+ block_authed_request_to_unauthorized_hostnames: {
432
+ if (!didSubstitute) {
433
+ break block_authed_request_to_unauthorized_hostnames;
434
+ }
435
+
436
+ if (
437
+ getIsHostnameAuthorized({
438
+ allowedHostnames: resourceServersAllowedHostnames,
439
+ extendAuthorizationToParentDomain: true,
440
+ hostname
441
+ })
442
+ ) {
443
+ break block_authed_request_to_unauthorized_hostnames;
444
+ }
445
+
446
+ throw new Error(
447
+ [
448
+ `oidc-spa: Blocked authed request to ${hostname}.`,
449
+ `To authorize this request add "${hostname}" to`,
450
+ "`resourceServersAllowedHostnames`."
451
+ ].join(" ")
452
+ );
453
+ }
454
+
455
+ const ws = new WebSocket_actual(nextUrl, nextProtocols as Parameters<typeof WebSocket>[1]);
456
+
457
+ wsDataByWs.set(ws, {
458
+ url: nextUrl,
459
+ hostname,
460
+ pathname,
461
+ didSubstitute
462
+ });
463
+
464
+ return ws;
465
+ };
466
+
467
+ WebSocketPatched.prototype = WebSocket_actual.prototype;
468
+
469
+ for (const name of ["CONNECTING", "OPEN", "CLOSING", "CLOSED"] as const) {
470
+ Object.defineProperty(WebSocketPatched, name, {
471
+ value: WebSocket_actual[name],
472
+ writable: false,
473
+ enumerable: true,
474
+ configurable: false
475
+ });
476
+ }
477
+
478
+ window.WebSocket = WebSocketPatched as unknown as typeof WebSocket;
479
+
480
+ WebSocket_actual.prototype.send = function send(data) {
481
+ const wsData = wsDataByWs.get(this);
482
+
483
+ if (wsData === undefined) {
484
+ // NOTE: This can happen for Vite's dev server websocket
485
+ return send_actual.call(this, data);
486
+ }
487
+
488
+ let nextData = data;
489
+
490
+ if (typeof data === "string") {
491
+ const nextDataText = substitutePlaceholderByRealToken(data);
492
+
493
+ if (nextDataText !== data) {
494
+ wsData.didSubstitute = true;
495
+ }
496
+
497
+ nextData = nextDataText;
498
+ }
499
+
500
+ block_authed_request_to_unauthorized_hostnames: {
501
+ if (!wsData.didSubstitute) {
502
+ break block_authed_request_to_unauthorized_hostnames;
503
+ }
504
+
505
+ if (
506
+ getIsHostnameAuthorized({
507
+ allowedHostnames: resourceServersAllowedHostnames,
508
+ extendAuthorizationToParentDomain: true,
509
+ hostname: wsData.hostname
510
+ })
511
+ ) {
512
+ break block_authed_request_to_unauthorized_hostnames;
513
+ }
514
+
515
+ throw new Error(
516
+ [
517
+ `oidc-spa: Blocked authed request to ${wsData.hostname}.`,
518
+ `To authorize this request add "${wsData.hostname}" to`,
519
+ "`resourceServersAllowedHostnames`."
520
+ ].join(" ")
521
+ );
522
+ }
523
+
524
+ prevent_fetching_of_hashed_js_assets: {
525
+ if (!viteHashedJsAssetPathRegExp.test(wsData.pathname)) {
526
+ break prevent_fetching_of_hashed_js_assets;
527
+ }
528
+
529
+ throw new Error("oidc-spa: Blocked request to hashed static asset.");
530
+ }
531
+
532
+ return send_actual.call(this, nextData);
533
+ };
534
+ }
535
+
536
+ function patchEventSourceApiToSubstituteTokenPlaceholder(params: {
537
+ resourceServersAllowedHostnames: string[];
538
+ }) {
539
+ const { resourceServersAllowedHostnames } = params;
540
+
541
+ const EventSource_actual = window.EventSource;
542
+
543
+ if (EventSource_actual === undefined) {
544
+ return;
545
+ }
546
+
547
+ const EventSourcePatched = function EventSource(
548
+ url: string | URL,
549
+ eventSourceInitDict?: EventSourceInit
550
+ ) {
551
+ const urlStr = typeof url === "string" ? url : url.href;
552
+ const nextUrl = substitutePlaceholderByRealToken(urlStr);
553
+ const didSubstitute = nextUrl !== urlStr;
554
+
555
+ const { hostname } = new URL(nextUrl, window.location.href);
556
+
557
+ block_authed_request_to_unauthorized_hostnames: {
558
+ if (!didSubstitute) {
559
+ break block_authed_request_to_unauthorized_hostnames;
560
+ }
561
+
562
+ if (
563
+ getIsHostnameAuthorized({
564
+ allowedHostnames: resourceServersAllowedHostnames,
565
+ extendAuthorizationToParentDomain: true,
566
+ hostname
567
+ })
568
+ ) {
569
+ break block_authed_request_to_unauthorized_hostnames;
570
+ }
571
+
572
+ throw new Error(
573
+ [
574
+ `oidc-spa: Blocked authed request to ${hostname}.`,
575
+ `To authorize this request add "${hostname}" to`,
576
+ "`resourceServersAllowedHostnames`."
577
+ ].join(" ")
578
+ );
579
+ }
580
+
581
+ return new EventSource_actual(nextUrl, eventSourceInitDict);
582
+ };
583
+
584
+ EventSourcePatched.prototype = EventSource_actual.prototype;
585
+
586
+ if ("CONNECTING" in EventSource_actual) {
587
+ for (const name of ["CONNECTING", "OPEN", "CLOSED"] as const) {
588
+ Object.defineProperty(EventSourcePatched, name, {
589
+ value: (EventSource_actual as any)[name],
590
+ writable: false,
591
+ enumerable: true,
592
+ configurable: false
593
+ });
594
+ }
595
+ }
596
+
597
+ window.EventSource = EventSourcePatched as unknown as typeof EventSource;
598
+ }
599
+
600
+ function patchNavigatorSendBeaconApiToSubstituteTokenPlaceholder(params: {
601
+ resourceServersAllowedHostnames: string[];
602
+ }) {
603
+ const { resourceServersAllowedHostnames } = params;
604
+
605
+ const sendBeacon_actual = navigator.sendBeacon?.bind(navigator);
606
+
607
+ if (sendBeacon_actual === undefined) {
608
+ return;
609
+ }
610
+
611
+ navigator.sendBeacon = function sendBeacon(url: string | URL, data?: BodyInit | null) {
612
+ const urlStr = typeof url === "string" ? url : url.href;
613
+ const nextUrl = substitutePlaceholderByRealToken(urlStr);
614
+ let didSubstitute = nextUrl !== urlStr;
615
+
616
+ const { hostname } = new URL(nextUrl, window.location.href);
617
+
618
+ let nextData = data;
619
+
620
+ if (typeof data === "string") {
621
+ const next = substitutePlaceholderByRealToken(data);
622
+
623
+ if (next !== data) {
624
+ didSubstitute = true;
625
+ }
626
+
627
+ nextData = next;
628
+ } else if (data instanceof URLSearchParams) {
629
+ let didUrlSearchParamsSubstitute = false;
630
+ const next = new URLSearchParams();
631
+
632
+ data.forEach((value, key) => {
633
+ const nextValue = substitutePlaceholderByRealToken(value);
634
+
635
+ if (nextValue !== value) {
636
+ didUrlSearchParamsSubstitute = true;
637
+ }
638
+
639
+ next.append(key, nextValue);
640
+ });
641
+
642
+ if (didUrlSearchParamsSubstitute) {
643
+ didSubstitute = true;
644
+ nextData = next;
645
+ }
646
+ } else if (data instanceof FormData) {
647
+ let didFormDataSubstitute = false;
648
+ const next = new FormData();
649
+
650
+ data.forEach((value, key) => {
651
+ if (typeof value === "string") {
652
+ const nextValue = substitutePlaceholderByRealToken(value);
653
+
654
+ if (nextValue !== value) {
655
+ didFormDataSubstitute = true;
656
+ }
657
+
658
+ next.append(key, nextValue);
659
+
660
+ return;
661
+ }
662
+
663
+ next.append(key, value);
664
+ });
665
+
666
+ if (didFormDataSubstitute) {
667
+ didSubstitute = true;
668
+ nextData = next;
669
+ }
670
+ }
671
+
672
+ block_authed_request_to_unauthorized_hostnames: {
673
+ if (!didSubstitute) {
674
+ break block_authed_request_to_unauthorized_hostnames;
675
+ }
676
+
677
+ if (
678
+ getIsHostnameAuthorized({
679
+ allowedHostnames: resourceServersAllowedHostnames,
680
+ extendAuthorizationToParentDomain: true,
681
+ hostname
682
+ })
683
+ ) {
684
+ break block_authed_request_to_unauthorized_hostnames;
685
+ }
686
+
687
+ throw new Error(
688
+ [
689
+ `oidc-spa: Blocked authed request to ${hostname}.`,
690
+ `To authorize this request add "${hostname}" to`,
691
+ "`resourceServersAllowedHostnames`."
692
+ ].join(" ")
693
+ );
694
+ }
695
+
696
+ return sendBeacon_actual(nextUrl, nextData as Parameters<typeof navigator.sendBeacon>[1]);
697
+ };
698
+ }
699
+
700
+ function runMonkeyPatchingPrevention() {
701
+ const createWriteError = (target: string) =>
702
+ new Error(
703
+ [
704
+ `oidc-spa: Monkey patching of ${target} has been blocked.`,
705
+ `Read: https://docs.oidc-spa.dev/v/v8/resources/blocked-monkey-patching`
706
+ ].join(" ")
707
+ );
708
+
709
+ for (const name of [
710
+ "fetch",
711
+ "XMLHttpRequest",
712
+ "WebSocket",
713
+ "Headers",
714
+ "URLSearchParams",
715
+ "EventSource",
716
+ "ServiceWorkerContainer",
717
+ "ServiceWorkerRegistration",
718
+ "ServiceWorker",
719
+ "FormData",
720
+ "Blob",
721
+ "String",
722
+ "Object",
723
+ "Promise",
724
+ "Array",
725
+ "RegExp",
726
+ "TextEncoder",
727
+ "Uint8Array",
728
+ "Uint32Array",
729
+ "Response",
730
+ "Reflect",
731
+ "JSON",
732
+ "encodeURIComponent",
733
+ "decodeURIComponent",
734
+ "atob",
735
+ "btoa"
736
+ ] as const) {
737
+ const original = window[name];
738
+
739
+ if (!original) {
740
+ continue;
741
+ }
742
+
743
+ if ("prototype" in original) {
744
+ for (const propertyName of Object.getOwnPropertyNames(original.prototype)) {
745
+ if (name === "Object") {
746
+ if (
747
+ propertyName === "toString" ||
748
+ propertyName === "constructor" ||
749
+ propertyName === "valueOf"
750
+ ) {
751
+ continue;
752
+ }
753
+ }
754
+
755
+ if (name === "Array") {
756
+ if (propertyName === "constructor" || propertyName === "concat") {
757
+ continue;
758
+ }
759
+ }
760
+
761
+ const pd = Object.getOwnPropertyDescriptor(original.prototype, propertyName);
762
+
763
+ assert(pd !== undefined);
764
+
765
+ if (!pd.configurable) {
766
+ continue;
767
+ }
768
+
769
+ Object.defineProperty(original.prototype, propertyName, {
770
+ enumerable: pd.enumerable,
771
+ configurable: false,
772
+ ...("value" in pd
773
+ ? {
774
+ get: () => pd.value,
775
+ set: () => {
776
+ throw createWriteError(`window.${name}.prototype.${propertyName}`);
777
+ }
778
+ }
779
+ : {
780
+ get: pd.get,
781
+ set:
782
+ pd.set ??
783
+ (() => {
784
+ throw createWriteError(`window.${name}.prototype.${propertyName}`);
785
+ })
786
+ })
787
+ });
788
+ }
789
+ }
790
+
791
+ Object.defineProperty(window, name, {
792
+ configurable: false,
793
+ enumerable: true,
794
+ get: () => original,
795
+ set: () => {
796
+ throw createWriteError(`window.${name}`);
797
+ }
798
+ });
799
+ }
800
+
801
+ {
802
+ const name = "serviceWorker";
803
+
804
+ const original = navigator[name];
805
+
806
+ Object.defineProperty(navigator, name, {
807
+ configurable: false,
808
+ enumerable: true,
809
+ get: () => original,
810
+ set: () => {
811
+ throw createWriteError(`window.navigator.${name}`);
812
+ }
813
+ });
814
+ }
815
+
816
+ for (const name of ["call", "apply", "bind"] as const) {
817
+ const original = Function.prototype[name];
818
+
819
+ Object.defineProperty(Function.prototype, name, {
820
+ configurable: false,
821
+ enumerable: true,
822
+ get: () => original,
823
+ set: () => {
824
+ throw createWriteError(`window.Function.prototype.${name})`);
825
+ }
826
+ });
827
+ }
828
+ }
829
+
830
+ function restrictServiceWorkerRegistration(params: { serviceWorkersAllowedHostnames: string[] }) {
831
+ const { serviceWorkersAllowedHostnames } = params;
832
+
833
+ const { serviceWorker } = navigator;
834
+
835
+ const register_actual = serviceWorker.register.bind(serviceWorker);
836
+
837
+ serviceWorker.register = function register(
838
+ scriptURL: Parameters<ServiceWorkerContainer["register"]>[0],
839
+ options?: Parameters<ServiceWorkerContainer["register"]>[1]
840
+ ) {
841
+ const { hostname, protocol } = new URL(
842
+ typeof scriptURL === "string" ? scriptURL : scriptURL.href,
843
+ window.location.href
844
+ );
845
+
846
+ if (protocol === "blob:") {
847
+ throw new Error(
848
+ [
849
+ "oidc-spa: Blocked service worker registration from blob.",
850
+ "Only solution: Set enableTokenExfiltrationDefense to false",
851
+ "or load the worker script from a remote url."
852
+ ].join(" ")
853
+ );
854
+ }
855
+
856
+ if (
857
+ !getIsHostnameAuthorized({
858
+ allowedHostnames: serviceWorkersAllowedHostnames,
859
+ extendAuthorizationToParentDomain: false,
860
+ hostname
861
+ })
862
+ ) {
863
+ throw new Error(
864
+ [
865
+ `oidc-spa: Blocked service worker registration to ${hostname}.`,
866
+ `To authorize this registration add "${hostname}" to`,
867
+ "`serviceWorkersAllowedHostnames`."
868
+ ].join(" ")
869
+ );
870
+ }
871
+
872
+ return register_actual(scriptURL, options);
873
+ };
874
+ }