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.
- package/backend.d.ts +3 -20
- package/backend.js +50 -242
- package/backend.js.map +1 -1
- package/core/OidcMetadata.d.ts +2 -2
- package/core/OidcMetadata.js.map +1 -1
- package/core/createOidc.d.ts +2 -4
- package/core/createOidc.js +49 -3
- package/core/createOidc.js.map +1 -1
- package/core/dpop.d.ts +20 -0
- package/core/dpop.js +389 -0
- package/core/dpop.js.map +1 -0
- package/core/earlyInit.js +2 -0
- package/core/earlyInit.js.map +1 -1
- package/core/oidcClientTsUserToTokens.d.ts +1 -0
- package/core/oidcClientTsUserToTokens.js +15 -5
- package/core/oidcClientTsUserToTokens.js.map +1 -1
- package/core/tokenExfiltrationDefense.js +49 -6
- package/core/tokenExfiltrationDefense.js.map +1 -1
- package/esm/angular.d.ts +2 -0
- package/esm/angular.mjs.map +1 -1
- package/esm/backend.d.ts +3 -20
- package/esm/backend.mjs +50 -242
- package/esm/backend.mjs.map +1 -1
- package/esm/core/OidcMetadata.d.ts +2 -2
- package/esm/core/OidcMetadata.mjs.map +1 -1
- package/esm/core/createOidc.d.ts +2 -4
- package/esm/core/createOidc.mjs +49 -3
- package/esm/core/createOidc.mjs.map +1 -1
- package/esm/core/dpop.d.ts +20 -0
- package/esm/core/dpop.mjs +384 -0
- package/esm/core/dpop.mjs.map +1 -0
- package/esm/core/earlyInit.mjs +2 -0
- package/esm/core/earlyInit.mjs.map +1 -1
- package/esm/core/oidcClientTsUserToTokens.d.ts +1 -0
- package/esm/core/oidcClientTsUserToTokens.mjs +15 -5
- package/esm/core/oidcClientTsUserToTokens.mjs.map +1 -1
- package/esm/core/tokenExfiltrationDefense.mjs +49 -6
- package/esm/core/tokenExfiltrationDefense.mjs.map +1 -1
- package/esm/react-spa/createOidcSpaApi.mjs +2 -1
- package/esm/react-spa/createOidcSpaApi.mjs.map +1 -1
- package/esm/react-spa/types.d.ts +2 -0
- package/esm/server/createOidcSpaUtils.d.ts +5 -0
- package/esm/server/createOidcSpaUtils.mjs +639 -0
- package/esm/server/createOidcSpaUtils.mjs.map +1 -0
- package/esm/server/index.d.ts +2 -0
- package/esm/server/index.mjs +3 -0
- package/esm/server/index.mjs.map +1 -0
- package/esm/server/types.d.ts +79 -0
- package/esm/server/types.mjs +2 -0
- package/esm/server/types.mjs.map +1 -0
- package/esm/server/utilsBuilder.d.ts +10 -0
- package/esm/server/utilsBuilder.mjs +13 -0
- package/esm/server/utilsBuilder.mjs.map +1 -0
- package/esm/tanstack-start/react/accessTokenValidation_rfc9068.d.ts +1 -1
- package/esm/tanstack-start/react/accessTokenValidation_rfc9068.mjs +102 -94
- package/esm/tanstack-start/react/accessTokenValidation_rfc9068.mjs.map +1 -1
- package/esm/tanstack-start/react/createOidcSpaApi.d.ts +2 -2
- package/esm/tanstack-start/react/createOidcSpaApi.mjs +60 -51
- package/esm/tanstack-start/react/createOidcSpaApi.mjs.map +1 -1
- package/esm/tanstack-start/react/index.d.ts +1 -1
- package/esm/tanstack-start/react/index.mjs +2 -2
- package/esm/tanstack-start/react/index.mjs.map +1 -1
- package/esm/tanstack-start/react/types.d.ts +36 -11
- package/esm/tanstack-start/react/{apiBuilder.d.ts → utilsBuilder.d.ts} +9 -9
- package/esm/tanstack-start/react/{apiBuilder.mjs → utilsBuilder.mjs} +6 -6
- package/esm/tanstack-start/react/utilsBuilder.mjs.map +1 -0
- package/esm/tools/generateES256DPoPProof.d.ts +8 -0
- package/esm/tools/generateES256DPoPProof.mjs +48 -0
- package/esm/tools/generateES256DPoPProof.mjs.map +1 -0
- package/esm/tools/getServerDateNow.d.ts +5 -0
- package/esm/tools/getServerDateNow.mjs +7 -0
- package/esm/tools/getServerDateNow.mjs.map +1 -0
- package/esm/vendor/{backend → server}/evt.mjs +84 -140
- package/esm/vendor/{backend → server}/jose.mjs +5 -27
- package/esm/vendor/{backend → server}/tsafe.d.ts +1 -0
- package/esm/vendor/{backend → server}/tsafe.mjs +6 -0
- package/esm/vendor/{backend → server}/zod.mjs +196 -50
- package/package.json +6 -1
- package/react-spa/createOidcSpaApi.js +2 -1
- package/react-spa/createOidcSpaApi.js.map +1 -1
- package/react-spa/types.d.ts +2 -0
- package/server/createOidcSpaUtils.d.ts +5 -0
- package/server/createOidcSpaUtils.js +642 -0
- package/server/createOidcSpaUtils.js.map +1 -0
- package/server/index.d.ts +2 -0
- package/server/index.js +6 -0
- package/server/index.js.map +1 -0
- package/server/types.d.ts +79 -0
- package/server/types.js +3 -0
- package/server/types.js.map +1 -0
- package/server/utilsBuilder.d.ts +10 -0
- package/server/utilsBuilder.js +16 -0
- package/server/utilsBuilder.js.map +1 -0
- package/src/angular.ts +3 -0
- package/src/backend.ts +63 -364
- package/src/core/OidcMetadata.ts +4 -2
- package/src/core/createOidc.ts +62 -6
- package/src/core/dpop.ts +583 -0
- package/src/core/earlyInit.ts +3 -0
- package/src/core/oidcClientTsUserToTokens.ts +18 -4
- package/src/core/tokenExfiltrationDefense.ts +60 -5
- package/src/react-spa/createOidcSpaApi.ts +2 -1
- package/src/react-spa/types.tsx +3 -0
- package/src/server/createOidcSpaUtils.ts +848 -0
- package/src/server/index.ts +4 -0
- package/src/server/types.tsx +99 -0
- package/src/server/utilsBuilder.ts +41 -0
- package/src/tanstack-start/react/accessTokenValidation_rfc9068.ts +134 -124
- package/src/tanstack-start/react/createOidcSpaApi.ts +73 -69
- package/src/tanstack-start/react/index.ts +2 -2
- package/src/tanstack-start/react/types.tsx +44 -12
- package/src/tanstack-start/react/{apiBuilder.ts → utilsBuilder.ts} +14 -14
- package/src/tools/generateES256DPoPProof.ts +74 -0
- package/src/tools/getServerDateNow.ts +11 -0
- package/src/vendor/{backend → server}/tsafe.ts +1 -0
- package/tools/generateES256DPoPProof.d.ts +8 -0
- package/tools/generateES256DPoPProof.js +51 -0
- package/tools/generateES256DPoPProof.js.map +1 -0
- package/tools/getServerDateNow.d.ts +5 -0
- package/tools/getServerDateNow.js +10 -0
- package/tools/getServerDateNow.js.map +1 -0
- package/vendor/server/evt.js +3 -0
- package/vendor/server/jose.js +3 -0
- package/vendor/{backend → server}/tsafe.d.ts +1 -0
- package/vendor/server/tsafe.js +2 -0
- package/vendor/server/zod.js +3 -0
- package/esm/tanstack-start/react/apiBuilder.mjs.map +0 -1
- package/vendor/backend/evt.js +0 -3
- package/vendor/backend/jose.js +0 -3
- package/vendor/backend/tsafe.js +0 -2
- package/vendor/backend/zod.js +0 -3
- /package/esm/vendor/{backend → server}/evt.d.ts +0 -0
- /package/esm/vendor/{backend → server}/jose.d.ts +0 -0
- /package/esm/vendor/{backend → server}/zod.d.ts +0 -0
- /package/src/vendor/{backend → server}/evt.ts +0 -0
- /package/src/vendor/{backend → server}/jose.ts +0 -0
- /package/src/vendor/{backend → server}/zod.ts +0 -0
- /package/vendor/{backend → server}/evt.d.ts +0 -0
- /package/vendor/{backend → server}/jose.d.ts +0 -0
- /package/vendor/{backend → server}/zod.d.ts +0 -0
package/src/core/createOidc.ts
CHANGED
|
@@ -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
|
|
package/src/core/dpop.ts
ADDED
|
@@ -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
|
+
}
|