oidc-spa 8.6.19 → 8.7.1

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 (140) hide show
  1. package/backend.d.ts +3 -20
  2. package/backend.js +50 -242
  3. package/backend.js.map +1 -1
  4. package/core/OidcMetadata.d.ts +2 -2
  5. package/core/OidcMetadata.js.map +1 -1
  6. package/core/createOidc.d.ts +2 -4
  7. package/core/createOidc.js +49 -3
  8. package/core/createOidc.js.map +1 -1
  9. package/core/dpop.d.ts +20 -0
  10. package/core/dpop.js +389 -0
  11. package/core/dpop.js.map +1 -0
  12. package/core/earlyInit.js +2 -0
  13. package/core/earlyInit.js.map +1 -1
  14. package/core/oidcClientTsUserToTokens.d.ts +1 -0
  15. package/core/oidcClientTsUserToTokens.js +15 -5
  16. package/core/oidcClientTsUserToTokens.js.map +1 -1
  17. package/core/tokenExfiltrationDefense.js +49 -6
  18. package/core/tokenExfiltrationDefense.js.map +1 -1
  19. package/esm/angular.d.ts +2 -0
  20. package/esm/angular.mjs.map +1 -1
  21. package/esm/backend.d.ts +3 -20
  22. package/esm/backend.mjs +50 -242
  23. package/esm/backend.mjs.map +1 -1
  24. package/esm/core/OidcMetadata.d.ts +2 -2
  25. package/esm/core/OidcMetadata.mjs.map +1 -1
  26. package/esm/core/createOidc.d.ts +2 -4
  27. package/esm/core/createOidc.mjs +49 -3
  28. package/esm/core/createOidc.mjs.map +1 -1
  29. package/esm/core/dpop.d.ts +20 -0
  30. package/esm/core/dpop.mjs +384 -0
  31. package/esm/core/dpop.mjs.map +1 -0
  32. package/esm/core/earlyInit.mjs +2 -0
  33. package/esm/core/earlyInit.mjs.map +1 -1
  34. package/esm/core/oidcClientTsUserToTokens.d.ts +1 -0
  35. package/esm/core/oidcClientTsUserToTokens.mjs +15 -5
  36. package/esm/core/oidcClientTsUserToTokens.mjs.map +1 -1
  37. package/esm/core/tokenExfiltrationDefense.mjs +49 -6
  38. package/esm/core/tokenExfiltrationDefense.mjs.map +1 -1
  39. package/esm/react-spa/createOidcSpaApi.mjs +2 -1
  40. package/esm/react-spa/createOidcSpaApi.mjs.map +1 -1
  41. package/esm/react-spa/types.d.ts +2 -0
  42. package/esm/server/createOidcSpaUtils.d.ts +5 -0
  43. package/esm/server/createOidcSpaUtils.mjs +639 -0
  44. package/esm/server/createOidcSpaUtils.mjs.map +1 -0
  45. package/esm/server/index.d.ts +2 -0
  46. package/esm/server/index.mjs +3 -0
  47. package/esm/server/index.mjs.map +1 -0
  48. package/esm/server/types.d.ts +79 -0
  49. package/esm/server/types.mjs +2 -0
  50. package/esm/server/types.mjs.map +1 -0
  51. package/esm/server/utilsBuilder.d.ts +10 -0
  52. package/esm/server/utilsBuilder.mjs +13 -0
  53. package/esm/server/utilsBuilder.mjs.map +1 -0
  54. package/esm/tanstack-start/react/accessTokenValidation_rfc9068.d.ts +1 -1
  55. package/esm/tanstack-start/react/accessTokenValidation_rfc9068.mjs +102 -94
  56. package/esm/tanstack-start/react/accessTokenValidation_rfc9068.mjs.map +1 -1
  57. package/esm/tanstack-start/react/createOidcSpaApi.d.ts +2 -2
  58. package/esm/tanstack-start/react/createOidcSpaApi.mjs +60 -51
  59. package/esm/tanstack-start/react/createOidcSpaApi.mjs.map +1 -1
  60. package/esm/tanstack-start/react/index.d.ts +1 -1
  61. package/esm/tanstack-start/react/index.mjs +2 -2
  62. package/esm/tanstack-start/react/index.mjs.map +1 -1
  63. package/esm/tanstack-start/react/types.d.ts +36 -11
  64. package/esm/tanstack-start/react/{apiBuilder.d.ts → utilsBuilder.d.ts} +9 -9
  65. package/esm/tanstack-start/react/{apiBuilder.mjs → utilsBuilder.mjs} +6 -6
  66. package/esm/tanstack-start/react/utilsBuilder.mjs.map +1 -0
  67. package/esm/tools/generateES256DPoPProof.d.ts +8 -0
  68. package/esm/tools/generateES256DPoPProof.mjs +48 -0
  69. package/esm/tools/generateES256DPoPProof.mjs.map +1 -0
  70. package/esm/tools/getServerDateNow.d.ts +5 -0
  71. package/esm/tools/getServerDateNow.mjs +7 -0
  72. package/esm/tools/getServerDateNow.mjs.map +1 -0
  73. package/esm/vendor/{backend → server}/evt.mjs +84 -140
  74. package/esm/vendor/{backend → server}/jose.mjs +5 -27
  75. package/esm/vendor/{backend → server}/tsafe.d.ts +1 -0
  76. package/esm/vendor/{backend → server}/tsafe.mjs +6 -0
  77. package/esm/vendor/{backend → server}/zod.mjs +196 -50
  78. package/package.json +6 -1
  79. package/react-spa/createOidcSpaApi.js +2 -1
  80. package/react-spa/createOidcSpaApi.js.map +1 -1
  81. package/react-spa/types.d.ts +2 -0
  82. package/server/createOidcSpaUtils.d.ts +5 -0
  83. package/server/createOidcSpaUtils.js +642 -0
  84. package/server/createOidcSpaUtils.js.map +1 -0
  85. package/server/index.d.ts +2 -0
  86. package/server/index.js +6 -0
  87. package/server/index.js.map +1 -0
  88. package/server/types.d.ts +79 -0
  89. package/server/types.js +3 -0
  90. package/server/types.js.map +1 -0
  91. package/server/utilsBuilder.d.ts +10 -0
  92. package/server/utilsBuilder.js +16 -0
  93. package/server/utilsBuilder.js.map +1 -0
  94. package/src/angular.ts +3 -0
  95. package/src/backend.ts +63 -364
  96. package/src/core/OidcMetadata.ts +4 -2
  97. package/src/core/createOidc.ts +62 -6
  98. package/src/core/dpop.ts +583 -0
  99. package/src/core/earlyInit.ts +3 -0
  100. package/src/core/oidcClientTsUserToTokens.ts +18 -4
  101. package/src/core/tokenExfiltrationDefense.ts +60 -5
  102. package/src/react-spa/createOidcSpaApi.ts +2 -1
  103. package/src/react-spa/types.tsx +3 -0
  104. package/src/server/createOidcSpaUtils.ts +848 -0
  105. package/src/server/index.ts +4 -0
  106. package/src/server/types.tsx +99 -0
  107. package/src/server/utilsBuilder.ts +41 -0
  108. package/src/tanstack-start/react/accessTokenValidation_rfc9068.ts +134 -124
  109. package/src/tanstack-start/react/createOidcSpaApi.ts +73 -69
  110. package/src/tanstack-start/react/index.ts +2 -2
  111. package/src/tanstack-start/react/types.tsx +44 -12
  112. package/src/tanstack-start/react/{apiBuilder.ts → utilsBuilder.ts} +14 -14
  113. package/src/tools/generateES256DPoPProof.ts +74 -0
  114. package/src/tools/getServerDateNow.ts +11 -0
  115. package/src/vendor/{backend → server}/tsafe.ts +1 -0
  116. package/tools/generateES256DPoPProof.d.ts +8 -0
  117. package/tools/generateES256DPoPProof.js +51 -0
  118. package/tools/generateES256DPoPProof.js.map +1 -0
  119. package/tools/getServerDateNow.d.ts +5 -0
  120. package/tools/getServerDateNow.js +10 -0
  121. package/tools/getServerDateNow.js.map +1 -0
  122. package/vendor/server/evt.js +3 -0
  123. package/vendor/server/jose.js +3 -0
  124. package/vendor/{backend → server}/tsafe.d.ts +1 -0
  125. package/vendor/server/tsafe.js +2 -0
  126. package/vendor/server/zod.js +3 -0
  127. package/esm/tanstack-start/react/apiBuilder.mjs.map +0 -1
  128. package/vendor/backend/evt.js +0 -3
  129. package/vendor/backend/jose.js +0 -3
  130. package/vendor/backend/tsafe.js +0 -2
  131. package/vendor/backend/zod.js +0 -3
  132. /package/esm/vendor/{backend → server}/evt.d.ts +0 -0
  133. /package/esm/vendor/{backend → server}/jose.d.ts +0 -0
  134. /package/esm/vendor/{backend → server}/zod.d.ts +0 -0
  135. /package/src/vendor/{backend → server}/evt.ts +0 -0
  136. /package/src/vendor/{backend → server}/jose.ts +0 -0
  137. /package/src/vendor/{backend → server}/zod.ts +0 -0
  138. /package/vendor/{backend → server}/evt.d.ts +0 -0
  139. /package/vendor/{backend → server}/jose.d.ts +0 -0
  140. /package/vendor/{backend → server}/zod.d.ts +0 -0
@@ -59,6 +59,7 @@ import {
59
59
  getIsStateDataCookieEnabled
60
60
  } from "./StateDataCookie";
61
61
  import { getIsTokenSubstitutionEnabled } from "./tokenPlaceholderSubstitution";
62
+ import { createInMemoryDPoPStore } from "./dpop";
62
63
  import { loadWebcryptoLinerShim } from "../tools/loadWebcryptoLinerShim";
63
64
 
64
65
  // NOTE: Replaced at build time
@@ -133,10 +134,6 @@ export type ParamsOfCreateOidc<
133
134
  idleSessionLifetimeInSeconds?: number;
134
135
 
135
136
  /**
136
- * Usage discouraged, this parameter exists because we don't want to assume
137
- * too much about your usecase but I can't think of a scenario where you would
138
- * want anything other than the current page.
139
- *
140
137
  * Default: { redirectTo: "current page" }
141
138
  */
142
139
  autoLogoutParams?: Parameters<Oidc.LoggedIn<any>["logout"]>[0];
@@ -247,6 +244,9 @@ export type ParamsOfCreateOidc<
247
244
  * API and no iframe capabilities.
248
245
  */
249
246
  postLoginRedirectUrl?: string;
247
+
248
+ /** See: https://docs.oidc-spa.dev/v/v8/features/dpop */
249
+ dpop?: "disabled" | "enabled" | "auto";
250
250
  };
251
251
 
252
252
  const globalContext = {
@@ -380,7 +380,8 @@ export async function createOidc_nonMemoized<
380
380
  __unsafe_clientSecret,
381
381
  __unsafe_useIdTokenAsAccessToken = false,
382
382
  __metadata,
383
- sessionRestorationMethod = params.autoLogin === true ? "full page redirect" : "auto"
383
+ sessionRestorationMethod = params.autoLogin === true ? "full page redirect" : "auto",
384
+ dpop
384
385
  } = params;
385
386
 
386
387
  const scopes = Array.from(new Set(["openid", ...(params.scopes ?? ["profile"])]));
@@ -454,6 +455,53 @@ export async function createOidc_nonMemoized<
454
455
 
455
456
  const oidcMetadata = __metadata ?? (await fetchOidcMetadata({ issuerUri }));
456
457
 
458
+ const isDPoPEnabled = (() => {
459
+ if (dpop === undefined) {
460
+ log?.("DPoP disabled, to enable it see: https://docs.oidc-spa.dev/features/dpop");
461
+ return false;
462
+ }
463
+
464
+ if (dpop === "disabled") {
465
+ log?.("DPoP explicitly disabled");
466
+ return false;
467
+ }
468
+
469
+ if (oidcMetadata === undefined) {
470
+ return false;
471
+ }
472
+
473
+ if (__unsafe_useIdTokenAsAccessToken) {
474
+ if (dpop === "enabled") {
475
+ throw new Error(
476
+ [
477
+ "oidc-spa: Cannot enable DPoP when",
478
+ "__unsafe_useIdTokenAsAccessToken is set to true"
479
+ ].join(" ")
480
+ );
481
+ }
482
+ log?.("DPoP Disabled due to __unsafe_useIdTokenAsAccessToken: true");
483
+ return false;
484
+ }
485
+
486
+ const isSupported = (() => {
487
+ const { dpop_signing_alg_values_supported } = oidcMetadata;
488
+
489
+ if (dpop_signing_alg_values_supported === undefined) {
490
+ return false;
491
+ }
492
+
493
+ return dpop_signing_alg_values_supported.includes("ES256");
494
+ })();
495
+
496
+ if (!isSupported) {
497
+ log?.("DPoP disabled because it's not supported by your IdP");
498
+ } else {
499
+ log?.("DPoP enabled");
500
+ }
501
+
502
+ return isSupported;
503
+ })();
504
+
457
505
  const canUseIframe = (() => {
458
506
  switch (sessionRestorationMethod) {
459
507
  case "auto":
@@ -666,7 +714,13 @@ export async function createOidc_nonMemoized<
666
714
  prefix: STATE_STORE_KEY_PREFIX
667
715
  }),
668
716
  client_secret: __unsafe_clientSecret,
669
- metadata: oidcMetadata
717
+ metadata: oidcMetadata,
718
+ dpop: !isDPoPEnabled
719
+ ? undefined
720
+ : {
721
+ bind_authorization_code: false,
722
+ store: createInMemoryDPoPStore({ configId })
723
+ }
670
724
  });
671
725
 
672
726
  const evtInitializationOutcomeUserNotLoggedIn = createEvt<void>();
@@ -1245,6 +1299,7 @@ export async function createOidc_nonMemoized<
1245
1299
  decodedIdTokenSchema,
1246
1300
  __unsafe_useIdTokenAsAccessToken,
1247
1301
  decodedIdToken_previous: undefined,
1302
+ isDPoPEnabled,
1248
1303
  log
1249
1304
  });
1250
1305
 
@@ -1572,6 +1627,7 @@ export async function createOidc_nonMemoized<
1572
1627
  decodedIdTokenSchema,
1573
1628
  __unsafe_useIdTokenAsAccessToken,
1574
1629
  decodedIdToken_previous: currentTokens.decodedIdToken,
1630
+ isDPoPEnabled,
1575
1631
  log
1576
1632
  });
1577
1633
 
@@ -0,0 +1,583 @@
1
+ import { assert } from "../tools/tsafe/assert";
2
+ import { generateES256DPoPProof } from "../tools/generateES256DPoPProof";
3
+ import { createGetServerDateNow, type ParamsOfCreateGetServerDateNow } from "../tools/getServerDateNow";
4
+
5
+ export type DPoPStore = {
6
+ set: (key: string, value: DPoPState) => Promise<void>;
7
+ get: (key: string) => Promise<DPoPState>;
8
+ remove: (key: string) => Promise<DPoPState>;
9
+ getAllKeys: () => Promise<string[]>;
10
+ };
11
+
12
+ export type DPoPState = {
13
+ keys: CryptoKeyPair;
14
+ nonce?: string;
15
+ };
16
+
17
+ // NOTE: Using object instead of Map because Map is not freezed.
18
+ const dpopStateByConfigId: { [configId: string]: DPoPState | undefined } = {};
19
+
20
+ export function createInMemoryDPoPStore(params: { configId: string }): DPoPStore {
21
+ const { configId } = params;
22
+
23
+ let key_singleton: string | undefined = undefined;
24
+
25
+ const store: DPoPStore = {
26
+ set: (key, value) => {
27
+ if (key_singleton !== undefined) {
28
+ assert(key === key_singleton, "394303302");
29
+ }
30
+
31
+ key_singleton = key;
32
+
33
+ dpopStateByConfigId[configId] = value;
34
+
35
+ return Promise.resolve();
36
+ },
37
+ get: key => {
38
+ assert(key_singleton !== undefined, "49303403");
39
+ assert(key_singleton === key, "34023493");
40
+ const value = dpopStateByConfigId[configId];
41
+ assert(value !== undefined, "943023493");
42
+ return Promise.resolve(value);
43
+ },
44
+ remove: async key => {
45
+ const value = await store.get(key);
46
+ delete dpopStateByConfigId[configId];
47
+ return value;
48
+ },
49
+ getAllKeys: () => {
50
+ if (configId in dpopStateByConfigId) {
51
+ assert(key_singleton !== undefined, "39430338");
52
+ return Promise.resolve([key_singleton]);
53
+ } else {
54
+ return Promise.resolve([]);
55
+ }
56
+ }
57
+ };
58
+
59
+ return store;
60
+ }
61
+
62
+ const accessTokenConfigIdEntries: {
63
+ configId: string;
64
+ accessToken: string;
65
+ paramsOfCreateGetServerDateNow: ParamsOfCreateGetServerDateNow;
66
+ }[] = [];
67
+
68
+ export function registerAccessTokenForDPoP(params: {
69
+ configId: string;
70
+ accessToken: string;
71
+ paramsOfCreateGetServerDateNow: ParamsOfCreateGetServerDateNow;
72
+ }) {
73
+ const { configId, accessToken, paramsOfCreateGetServerDateNow } = params;
74
+
75
+ for (const entry of accessTokenConfigIdEntries) {
76
+ if (entry.configId !== configId) {
77
+ continue;
78
+ }
79
+
80
+ setTimeout(() => {
81
+ const index = accessTokenConfigIdEntries.indexOf(entry);
82
+
83
+ if (index === -1) {
84
+ return;
85
+ }
86
+
87
+ accessTokenConfigIdEntries.splice(index, 1);
88
+ }, 30_000);
89
+ }
90
+
91
+ const entry_new: (typeof accessTokenConfigIdEntries)[number] = {
92
+ configId,
93
+ accessToken,
94
+ paramsOfCreateGetServerDateNow
95
+ };
96
+
97
+ accessTokenConfigIdEntries.push(entry_new);
98
+ }
99
+
100
+ const nonceEntriesByConfigId: { [configId: string]: { origin: string; nonce: string }[] | undefined } =
101
+ {};
102
+
103
+ function generateMaterialToUpgradeBearerRequestToDPoP(params: {
104
+ httpMethod: string;
105
+ url: string;
106
+ authorizationHeaderValue: string | undefined;
107
+ }):
108
+ | {
109
+ isHandled: false;
110
+ }
111
+ | {
112
+ isHandled: true;
113
+ accessToken: string;
114
+ nextStepDPoP: () => Promise<{
115
+ dpopProof: string;
116
+ registerDPoPNonce: (params: { nonce: string }) => void;
117
+ reGenerateDpopProof: () => Promise<string>;
118
+ }>;
119
+ } {
120
+ const { httpMethod, url, authorizationHeaderValue } = params;
121
+
122
+ if (authorizationHeaderValue === undefined) {
123
+ return {
124
+ isHandled: false
125
+ };
126
+ }
127
+
128
+ const accessToken = (() => {
129
+ const match = authorizationHeaderValue.match(/^\s*Bearer\s+(.+?)\s*$/i);
130
+
131
+ if (match === null) {
132
+ return undefined;
133
+ }
134
+
135
+ return match[1];
136
+ })();
137
+
138
+ if (accessToken === undefined) {
139
+ return {
140
+ isHandled: false
141
+ };
142
+ }
143
+
144
+ const entry = accessTokenConfigIdEntries.find(entry => entry.accessToken === accessToken);
145
+
146
+ if (entry === undefined) {
147
+ return {
148
+ isHandled: false
149
+ };
150
+ }
151
+
152
+ const { configId, paramsOfCreateGetServerDateNow } = entry;
153
+
154
+ const dpopState = dpopStateByConfigId[configId];
155
+
156
+ assert(dpopState !== undefined, "304922047");
157
+
158
+ const nonceEntries = (nonceEntriesByConfigId[configId] ??= []);
159
+
160
+ const origin = new URL(url).origin;
161
+
162
+ const generateDPoPProof = () =>
163
+ generateES256DPoPProof({
164
+ keyPair: dpopState.keys,
165
+ url,
166
+ accessToken,
167
+ httpMethod,
168
+ nonce: nonceEntries.find(entry => entry.origin === origin)?.nonce,
169
+ getServerDateNow: createGetServerDateNow(paramsOfCreateGetServerDateNow)
170
+ });
171
+
172
+ return {
173
+ isHandled: true,
174
+ accessToken,
175
+ nextStepDPoP: async () => ({
176
+ dpopProof: await generateDPoPProof(),
177
+ registerDPoPNonce: ({ nonce }) => {
178
+ const nonceEntry = nonceEntries.find(entry => entry.origin === origin);
179
+
180
+ if (nonceEntry !== undefined) {
181
+ nonceEntry.nonce = nonce;
182
+ } else {
183
+ nonceEntries.push({ origin, nonce });
184
+ }
185
+ },
186
+ reGenerateDpopProof: generateDPoPProof
187
+ })
188
+ };
189
+ }
190
+
191
+ export function implementFetchAndXhrDPoPInterceptor() {
192
+ function readNonceFromResponseHeader(params: {
193
+ getResponseHeader: (headerName: string) => string | null;
194
+ }) {
195
+ const { getResponseHeader } = params;
196
+
197
+ dpop_nonce_header: {
198
+ const value = getResponseHeader("DPoP-Nonce");
199
+ if (value === null) {
200
+ break dpop_nonce_header;
201
+ }
202
+ return value;
203
+ }
204
+
205
+ www_authenticate_header: {
206
+ const value = getResponseHeader("WWW-Authenticate");
207
+
208
+ if (value === null) {
209
+ break www_authenticate_header;
210
+ }
211
+
212
+ {
213
+ const value_lower = value.toLowerCase();
214
+
215
+ if (!value_lower.includes("dpop") || !value_lower.includes("use_dpop_nonce")) {
216
+ break www_authenticate_header;
217
+ }
218
+ }
219
+
220
+ const match = value.match(/nonce="([^"]+)"/i);
221
+
222
+ if (match === null) {
223
+ break www_authenticate_header;
224
+ }
225
+
226
+ return match[1];
227
+ }
228
+
229
+ return undefined;
230
+ }
231
+
232
+ {
233
+ const createFetchProxy = (params: { fetch: typeof window.fetch; isFetchLater: boolean }) => {
234
+ const { fetch, isFetchLater } = params;
235
+
236
+ let hasLoggedFetchLaterWarning = false;
237
+
238
+ const fetchProxy: typeof fetch = async (input, init) => {
239
+ if (accessTokenConfigIdEntries.length === 0) {
240
+ return fetch(input, init);
241
+ }
242
+
243
+ let request = input instanceof Request ? input : new Request(input, init);
244
+
245
+ const fetch_ctx: (request: Request) => Promise<Response> = (() => {
246
+ if (!init) {
247
+ return fetch;
248
+ }
249
+
250
+ // @ts-expect-error
251
+ const { activateAfter } = init;
252
+
253
+ if (!activateAfter) {
254
+ return fetch;
255
+ }
256
+
257
+ return request => {
258
+ // @ts-expect-error
259
+ return fetch(request, { activateAfter });
260
+ };
261
+ })();
262
+
263
+ let dpopStatus:
264
+ | { isHandled: false }
265
+ | {
266
+ isHandled: true;
267
+ registerDPoPNonce: (params: { nonce: string }) => void;
268
+ reGenerateDpopProof: () => Promise<string>;
269
+ };
270
+
271
+ update_headers: {
272
+ const result = generateMaterialToUpgradeBearerRequestToDPoP({
273
+ authorizationHeaderValue: request.headers.get("Authorization") ?? undefined,
274
+ url: request.url,
275
+ httpMethod: request.method
276
+ });
277
+
278
+ if (!result.isHandled) {
279
+ dpopStatus = { isHandled: false };
280
+ break update_headers;
281
+ }
282
+
283
+ const { accessToken, nextStepDPoP } = result;
284
+
285
+ const { dpopProof, reGenerateDpopProof, registerDPoPNonce } = await nextStepDPoP();
286
+
287
+ request = new Request(request, {
288
+ headers: (() => {
289
+ const h = new Headers(request.headers);
290
+ h.set("Authorization", `DPoP ${accessToken}`);
291
+ h.set("DPoP", dpopProof);
292
+ return h;
293
+ })()
294
+ });
295
+
296
+ dpopStatus = {
297
+ isHandled: true,
298
+ registerDPoPNonce,
299
+ reGenerateDpopProof
300
+ };
301
+ }
302
+
303
+ if (!dpopStatus.isHandled) {
304
+ return fetch_ctx(request);
305
+ }
306
+
307
+ if (isFetchLater && !hasLoggedFetchLaterWarning) {
308
+ console.warn(
309
+ [
310
+ "oidc-spa: Detected an authenticated fetchLater() request while DPoP is enabled.",
311
+ "Support for fetchLater + DPoP is not fully implemented yet.",
312
+ "If you rely on this, please open an issue and we will implement support:",
313
+ "https://github.com/keycloakify/oidc-spa"
314
+ ].join(" ")
315
+ );
316
+ hasLoggedFetchLaterWarning = true;
317
+ }
318
+
319
+ let request_cloneForReplay = (() => {
320
+ const method = request.method.toUpperCase();
321
+
322
+ if (method !== "GET" && method !== "HEAD") {
323
+ return undefined;
324
+ }
325
+
326
+ try {
327
+ return request.clone();
328
+ } catch {
329
+ return undefined;
330
+ }
331
+ })();
332
+
333
+ let response = await fetch_ctx(request);
334
+
335
+ re_send_with_DPoP_nonce: {
336
+ if (response.status !== 401) {
337
+ break re_send_with_DPoP_nonce;
338
+ }
339
+
340
+ const nonce = readNonceFromResponseHeader({
341
+ getResponseHeader: headerName => response.headers.get(headerName)
342
+ });
343
+
344
+ if (nonce === undefined) {
345
+ break re_send_with_DPoP_nonce;
346
+ }
347
+
348
+ dpopStatus.registerDPoPNonce({ nonce });
349
+
350
+ if (request_cloneForReplay === undefined) {
351
+ break re_send_with_DPoP_nonce;
352
+ }
353
+
354
+ const dpopProof_new = await dpopStatus.reGenerateDpopProof();
355
+
356
+ response = await fetch_ctx(
357
+ new Request(request_cloneForReplay, {
358
+ headers: (() => {
359
+ const h = new Headers(request_cloneForReplay.headers);
360
+ h.set("DPoP", dpopProof_new);
361
+ return h;
362
+ })()
363
+ })
364
+ );
365
+ }
366
+
367
+ {
368
+ const nonce = readNonceFromResponseHeader({
369
+ getResponseHeader: headerName => response.headers.get(headerName)
370
+ });
371
+
372
+ if (nonce !== undefined) {
373
+ dpopStatus.registerDPoPNonce({ nonce });
374
+ }
375
+ }
376
+
377
+ return response;
378
+ };
379
+
380
+ return fetchProxy;
381
+ };
382
+
383
+ window.fetch = createFetchProxy({ fetch: window.fetch, isFetchLater: false });
384
+
385
+ // @ts-expect-error
386
+ if (window.fetchLater) {
387
+ // @ts-expect-error
388
+ window.fetchLater = createFetchProxy({ fetch: window.fetchLater, isFetchLater: true });
389
+ }
390
+ }
391
+ {
392
+ const XMLHttpRequest_prototype_actual = {
393
+ open: XMLHttpRequest.prototype.open,
394
+ send: XMLHttpRequest.prototype.send,
395
+ setRequestHeader: XMLHttpRequest.prototype.setRequestHeader
396
+ };
397
+
398
+ const stateByInstance = new WeakMap<
399
+ XMLHttpRequest,
400
+ {
401
+ method: string;
402
+ url: string;
403
+ isSynchronous: boolean;
404
+ handledRequestState:
405
+ | {
406
+ accessToken: string;
407
+ nextStepDPoP: () => Promise<{
408
+ dpopProof: string;
409
+ registerDPoPNonce: (params: { nonce: string }) => void;
410
+ reGenerateDpopProof: () => Promise<string>;
411
+ }>;
412
+ hasSendBeenCalled: boolean;
413
+ }
414
+ | undefined;
415
+ }
416
+ >();
417
+
418
+ XMLHttpRequest.prototype.open = function () {
419
+ const [method, url, async] = arguments as any as [string, string | URL, boolean | undefined];
420
+
421
+ if (stateByInstance.get(this)?.handledRequestState !== undefined) {
422
+ throw new Error(
423
+ [
424
+ "oidc-spa: Cannot reuse XMLHttpRequest instances",
425
+ "that have been DPoP upgraded"
426
+ ].join(" ")
427
+ );
428
+ }
429
+
430
+ stateByInstance.set(this, {
431
+ method: method.toUpperCase(),
432
+ url: typeof url === "string" ? new URL(url, window.location.href).href : url.href,
433
+ isSynchronous: async === false,
434
+ handledRequestState: undefined
435
+ });
436
+
437
+ return XMLHttpRequest_prototype_actual.open.apply(
438
+ this,
439
+ // @ts-expect-error
440
+ arguments
441
+ );
442
+ };
443
+
444
+ XMLHttpRequest.prototype.setRequestHeader = function (name, value) {
445
+ intercept: {
446
+ const state = stateByInstance.get(this);
447
+
448
+ if (state?.handledRequestState?.hasSendBeenCalled === true) {
449
+ throw new DOMException(
450
+ "Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.",
451
+ "InvalidStateError"
452
+ );
453
+ }
454
+
455
+ if (name.toLowerCase() !== "authorization") {
456
+ break intercept;
457
+ }
458
+
459
+ if (this.readyState !== XMLHttpRequest.OPENED) {
460
+ break intercept;
461
+ }
462
+
463
+ // NOTE: If it's opened we know open have been called and hence the state is set.
464
+ assert(state !== undefined, "34308330");
465
+
466
+ const result = generateMaterialToUpgradeBearerRequestToDPoP({
467
+ httpMethod: state.method,
468
+ url: state.url,
469
+ authorizationHeaderValue: value
470
+ });
471
+
472
+ if (!result.isHandled) {
473
+ break intercept;
474
+ }
475
+
476
+ if (state.handledRequestState !== undefined) {
477
+ throw new Error(
478
+ [
479
+ 'oidc-spa: Calling xhr.setRequestHeader("Authorization", `Bearer <access_token>`)',
480
+ "more than once on the same instance, this is probably an usage error"
481
+ ].join(" ")
482
+ );
483
+ }
484
+
485
+ if (state.isSynchronous) {
486
+ throw new Error(
487
+ [
488
+ "oidc-spa: Cannot perform synchronous authenticated XMLHttpRequest",
489
+ "requests when DPoP is enabled."
490
+ ].join(" ")
491
+ );
492
+ }
493
+
494
+ state.handledRequestState = {
495
+ accessToken: result.accessToken,
496
+ nextStepDPoP: result.nextStepDPoP,
497
+ hasSendBeenCalled: false
498
+ };
499
+
500
+ return;
501
+ }
502
+
503
+ return XMLHttpRequest_prototype_actual.setRequestHeader.apply(
504
+ this,
505
+ // @ts-expect-error
506
+ arguments
507
+ );
508
+ };
509
+
510
+ XMLHttpRequest.prototype.send = function () {
511
+ intercept: {
512
+ const state = stateByInstance.get(this);
513
+
514
+ if (state === undefined) {
515
+ break intercept;
516
+ }
517
+
518
+ const { handledRequestState } = state;
519
+
520
+ if (handledRequestState === undefined) {
521
+ break intercept;
522
+ }
523
+
524
+ if (handledRequestState.hasSendBeenCalled) {
525
+ throw new DOMException(
526
+ "Failed to execute 'send' on 'XMLHttpRequest': The object's state must be OPENED.",
527
+ "InvalidStateError"
528
+ );
529
+ }
530
+
531
+ handledRequestState.hasSendBeenCalled = true;
532
+
533
+ const { accessToken, nextStepDPoP } = handledRequestState;
534
+
535
+ nextStepDPoP().then(({ dpopProof, registerDPoPNonce }) => {
536
+ if (this.readyState !== XMLHttpRequest.OPENED) {
537
+ // abort() has been called.
538
+ return;
539
+ }
540
+
541
+ const onReadyStateChange = () => {
542
+ if (this.readyState !== XMLHttpRequest.DONE) {
543
+ return;
544
+ }
545
+
546
+ const nonce = readNonceFromResponseHeader({
547
+ getResponseHeader: headerName => this.getResponseHeader(headerName)
548
+ });
549
+
550
+ if (nonce !== undefined) {
551
+ registerDPoPNonce({ nonce });
552
+ }
553
+
554
+ this.removeEventListener("readystatechange", onReadyStateChange);
555
+ };
556
+
557
+ this.addEventListener("readystatechange", onReadyStateChange);
558
+
559
+ XMLHttpRequest_prototype_actual.setRequestHeader.call(
560
+ this,
561
+ "Authorization",
562
+ `DPoP ${accessToken}`
563
+ );
564
+ XMLHttpRequest_prototype_actual.setRequestHeader.call(this, "DPoP", dpopProof);
565
+
566
+ XMLHttpRequest_prototype_actual.send.apply(
567
+ this,
568
+ // @ts-expect-error
569
+ arguments
570
+ );
571
+ });
572
+
573
+ return;
574
+ }
575
+
576
+ return XMLHttpRequest_prototype_actual.send.apply(
577
+ this,
578
+ // @ts-expect-error
579
+ arguments
580
+ );
581
+ };
582
+ }
583
+ }