@web-ts-toolkit/oidc-vault-dpop-client 0.51.0 → 0.53.0
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/README.md +483 -73
- package/index.d.mts +182 -81
- package/index.d.ts +182 -81
- package/index.js +943 -226
- package/index.mjs +946 -229
- package/llms.txt +52 -37
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -45,21 +45,54 @@ var OidcVaultDpopClientError = class extends Error {
|
|
|
45
45
|
};
|
|
46
46
|
var keyLost = () => new OidcVaultDpopClientError("DPOP_KEY_LOST", "The browser login key is missing or changed. Sign in again.", true);
|
|
47
47
|
var loginRequired = () => new OidcVaultDpopClientError("LOGIN_REQUIRED", "Sign in to continue.", true);
|
|
48
|
-
var
|
|
48
|
+
var vaultRequestTimeout = () => new OidcVaultDpopClientError("DPOP_REQUEST_TIMEOUT", "The vault request timed out.");
|
|
49
|
+
var vaultResponseTooLarge = () => new OidcVaultDpopClientError("DPOP_RESPONSE_TOO_LARGE", "The vault response exceeded the size limit.");
|
|
50
|
+
var TERMINAL_SERVER_MESSAGES = Object.freeze({
|
|
49
51
|
OIDC_VAULT_FINGERPRINT_REAUTH_REQUIRED: "Browser recognition changed; sign in again.",
|
|
52
|
+
OIDC_VAULT_DEVICE_BINDING_REQUIRED: "A device-bound login is required.",
|
|
53
|
+
OIDC_VAULT_INVALID_BROWSER_BINDING: "Login browser binding validation failed.",
|
|
54
|
+
OIDC_VAULT_INVALID_SESSION: "Session is missing or expired.",
|
|
55
|
+
OIDC_VAULT_MISSING_SESSION_ID: "Session handle is missing.",
|
|
56
|
+
OIDC_VAULT_MALFORMED_SESSION_COOKIE: "Session cookie is malformed.",
|
|
57
|
+
OIDC_VAULT_INVALID_EXCHANGE_CODE: "Exchange code is invalid or expired."
|
|
58
|
+
});
|
|
59
|
+
var NON_TERMINAL_SERVER_MESSAGES = Object.freeze({
|
|
50
60
|
OIDC_VAULT_INVALID_FINGERPRINT: "Fingerprint signal is invalid.",
|
|
51
61
|
OIDC_VAULT_USE_DPOP_NONCE: "A fresh DPoP nonce is required.",
|
|
52
62
|
OIDC_VAULT_INVALID_DPOP_PROOF: "DPoP proof validation failed.",
|
|
53
63
|
OIDC_VAULT_DPOP_REQUIRED: "DPoP authentication is required.",
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
OIDC_VAULT_DPOP_REPLAY_UNAVAILABLE: "DPoP replay protection is unavailable.",
|
|
65
|
+
OIDC_VAULT_UNTRUSTED_ORIGIN: "The request origin is not trusted.",
|
|
66
|
+
OIDC_VAULT_INVALID_RETURN_TO: "The login return target is invalid.",
|
|
67
|
+
OIDC_VAULT_MISSING_EXCHANGE_CODE: "The exchange request is missing a code.",
|
|
68
|
+
OIDC_VAULT_REQUEST_BODY_TOO_LARGE: "The vault request body exceeds the size limit.",
|
|
69
|
+
OIDC_VAULT_REQUEST_BODY_PARAMETER_LIMIT_EXCEEDED: "The vault request has too many parameters.",
|
|
70
|
+
OIDC_VAULT_UNSUPPORTED_REQUEST_BODY_ENCODING: "The vault request body encoding is unsupported.",
|
|
71
|
+
OIDC_VAULT_UNSUPPORTED_REQUEST_BODY_TYPE: "The vault request body type is unsupported.",
|
|
72
|
+
OIDC_VAULT_MALFORMED_REQUEST_BODY: "The vault request body is malformed.",
|
|
73
|
+
OIDC_VAULT_INVALID_REQUEST_BODY: "The vault request body is invalid.",
|
|
74
|
+
OIDC_VAULT_INTERNAL_ERROR: "The OIDC request failed.",
|
|
75
|
+
OIDC_VAULT_INVALID_CONFIG: "The OIDC configuration is invalid.",
|
|
76
|
+
OIDC_VAULT_MISSING_CLIENT_ID: "The OIDC client configuration is incomplete.",
|
|
77
|
+
OIDC_VAULT_MISSING_JWKS_URI: "The OIDC signing-key configuration is incomplete.",
|
|
78
|
+
OIDC_VAULT_DISCOVERY_FAILED: "OIDC discovery failed.",
|
|
79
|
+
OIDC_VAULT_DISCOVERY_INVALID: "OIDC discovery metadata is invalid.",
|
|
80
|
+
OIDC_VAULT_JWKS_FAILED: "OIDC signing keys are unavailable.",
|
|
81
|
+
OIDC_VAULT_TOKEN_REQUEST_FAILED: "OIDC token request failed.",
|
|
82
|
+
OIDC_VAULT_INVALID_TOKEN_RESPONSE: "OIDC token response is invalid.",
|
|
83
|
+
OIDC_VAULT_INVALID_ID_TOKEN: "OIDC ID token validation failed.",
|
|
84
|
+
OIDC_VAULT_USERINFO_FAILED: "OIDC userinfo request failed.",
|
|
85
|
+
OIDC_VAULT_INVALID_USERINFO: "OIDC userinfo response is invalid."
|
|
58
86
|
});
|
|
59
87
|
var serverError = (status, value) => {
|
|
60
|
-
const code = typeof value === "object" && value !== null &&
|
|
61
|
-
|
|
62
|
-
|
|
88
|
+
const code = typeof value === "object" && value !== null && !Array.isArray(value) ? Object.getOwnPropertyDescriptor(value, "code")?.value : void 0;
|
|
89
|
+
if (typeof code === "string") {
|
|
90
|
+
if (Object.hasOwn(TERMINAL_SERVER_MESSAGES, code))
|
|
91
|
+
return new OidcVaultDpopClientError(code, TERMINAL_SERVER_MESSAGES[code], true, status);
|
|
92
|
+
if (Object.hasOwn(NON_TERMINAL_SERVER_MESSAGES, code))
|
|
93
|
+
return new OidcVaultDpopClientError(code, NON_TERMINAL_SERVER_MESSAGES[code], false, status);
|
|
94
|
+
}
|
|
95
|
+
return new OidcVaultDpopClientError("OIDC_REQUEST_FAILED", "The OIDC request failed.", false, status);
|
|
63
96
|
};
|
|
64
97
|
|
|
65
98
|
// src/key-database.ts
|
|
@@ -90,9 +123,93 @@ var withDpopDatabase = async (operation) => {
|
|
|
90
123
|
}
|
|
91
124
|
};
|
|
92
125
|
var readCookieSessionVersion = (scopeId) => withDpopDatabase((db) => db.get("cookieSessions", scopeId));
|
|
93
|
-
var
|
|
94
|
-
|
|
95
|
-
|
|
126
|
+
var guardDpopTransaction = async (transaction, authority, run) => {
|
|
127
|
+
let aborted = false;
|
|
128
|
+
const abort = () => {
|
|
129
|
+
if (aborted) return;
|
|
130
|
+
aborted = true;
|
|
131
|
+
try {
|
|
132
|
+
transaction.abort();
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
authority?.signal.addEventListener("abort", abort, { once: true });
|
|
137
|
+
try {
|
|
138
|
+
const [result] = await Promise.all([
|
|
139
|
+
Promise.resolve().then(() => {
|
|
140
|
+
authority?.assertCurrent();
|
|
141
|
+
return run();
|
|
142
|
+
}),
|
|
143
|
+
transaction.done
|
|
144
|
+
]);
|
|
145
|
+
authority?.assertCurrent();
|
|
146
|
+
return result;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
abort();
|
|
149
|
+
throw error;
|
|
150
|
+
} finally {
|
|
151
|
+
authority?.signal.removeEventListener("abort", abort);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
var writeCookieSessionVersion = async (scopeId, value, authority) => {
|
|
155
|
+
authority.assertCurrent();
|
|
156
|
+
await withDpopDatabase(async (db) => {
|
|
157
|
+
authority.assertCurrent();
|
|
158
|
+
const transaction = db.transaction("cookieSessions", "readwrite");
|
|
159
|
+
await guardDpopTransaction(transaction, authority, () => transaction.store.put(value, scopeId));
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
var scopeState = async (transaction, scopeId) => {
|
|
163
|
+
const keys = transaction.objectStore("keys");
|
|
164
|
+
const cookies = transaction.objectStore("cookieSessions");
|
|
165
|
+
const [keyId, key, cookieId, cookie] = await Promise.all([
|
|
166
|
+
Promise.resolve().then(() => keys.getKey(scopeId)),
|
|
167
|
+
Promise.resolve().then(() => keys.get(scopeId)),
|
|
168
|
+
Promise.resolve().then(() => cookies.getKey(scopeId)),
|
|
169
|
+
Promise.resolve().then(() => cookies.get(scopeId))
|
|
170
|
+
]);
|
|
171
|
+
return {
|
|
172
|
+
keyPresent: keyId !== void 0,
|
|
173
|
+
keyJkt: typeof key?.jkt === "string" ? key.jkt : void 0,
|
|
174
|
+
cookiePresent: cookieId !== void 0,
|
|
175
|
+
version: cookie && typeof cookie.jkt === "string" && typeof cookie.generation === "string" && typeof cookie.active === "boolean" ? { jkt: cookie.jkt, generation: cookie.generation, active: cookie.active } : void 0
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
var readDpopScopeState = async (scopeId, authority) => {
|
|
179
|
+
authority.assertCurrent();
|
|
180
|
+
return withDpopDatabase(async (db) => {
|
|
181
|
+
authority.assertCurrent();
|
|
182
|
+
const transaction = db.transaction(["keys", "cookieSessions"], "readonly");
|
|
183
|
+
return guardDpopTransaction(transaction, authority, () => scopeState(transaction, scopeId));
|
|
184
|
+
});
|
|
185
|
+
};
|
|
186
|
+
var removeDpopScope = async (scopeId, authority) => {
|
|
187
|
+
authority.assertCurrent();
|
|
188
|
+
try {
|
|
189
|
+
return await withDpopDatabase(async (db) => {
|
|
190
|
+
authority.assertCurrent();
|
|
191
|
+
const transaction = db.transaction(["keys", "cookieSessions"], "readwrite");
|
|
192
|
+
return guardDpopTransaction(transaction, authority, async () => {
|
|
193
|
+
const previous = await scopeState(transaction, scopeId);
|
|
194
|
+
authority.assertCurrent();
|
|
195
|
+
const remove = async (store) => {
|
|
196
|
+
authority.assertCurrent();
|
|
197
|
+
await transaction.objectStore(store).delete(scopeId);
|
|
198
|
+
};
|
|
199
|
+
await Promise.all([remove("keys"), remove("cookieSessions")]);
|
|
200
|
+
return previous;
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
} catch (error) {
|
|
204
|
+
authority.assertCurrent();
|
|
205
|
+
if (error instanceof OidcVaultDpopClientError) throw error;
|
|
206
|
+
throw new OidcVaultDpopClientError(
|
|
207
|
+
"DPOP_KEY_STORAGE_UNAVAILABLE",
|
|
208
|
+
"IndexedDB key persistence is unavailable.",
|
|
209
|
+
true
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
96
213
|
|
|
97
214
|
// src/scope.ts
|
|
98
215
|
var invalidTarget = () => new OidcVaultDpopClientError("INVALID_DPOP_TARGET", "A pinned absolute HTTP(S) target is required.");
|
|
@@ -125,12 +242,21 @@ var normalizeDpopTarget = (value) => {
|
|
|
125
242
|
var normalizeStaticOrigin = (value) => {
|
|
126
243
|
const target = normalizeDpopTarget(value);
|
|
127
244
|
const url = new URL(value);
|
|
128
|
-
if (url.pathname !== "/" || url.search || url.hash || target !== `${url.origin}/`)
|
|
245
|
+
if (!/^https?:\/\/[^/\\?#]+\/?$/i.test(value) || url.pathname !== "/" || url.search || url.hash || target !== `${url.origin}/`) {
|
|
246
|
+
throw invalidTarget();
|
|
247
|
+
}
|
|
129
248
|
if (url.protocol !== "https:" && !["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)) {
|
|
130
249
|
throw new OidcVaultDpopClientError("INSECURE_DPOP_ORIGIN", "DPoP requires HTTPS or loopback development.");
|
|
131
250
|
}
|
|
132
251
|
return url.origin;
|
|
133
252
|
};
|
|
253
|
+
var normalizeDpopPathPrefix = (origin, value) => {
|
|
254
|
+
if (value === void 0) return "/";
|
|
255
|
+
if (typeof value !== "string" || value.startsWith("//") || /[?#]/.test(value)) throw invalidTarget();
|
|
256
|
+
const path = normalizePath(origin, value);
|
|
257
|
+
if (path.startsWith("//")) throw invalidTarget();
|
|
258
|
+
return path.replace(/\/+$/g, "") || "/";
|
|
259
|
+
};
|
|
134
260
|
var resolveDpopScope = (scope) => {
|
|
135
261
|
const frontendOrigin = normalizeStaticOrigin(scope.frontendOrigin);
|
|
136
262
|
const backendOrigin = normalizeStaticOrigin(scope.backendOrigin);
|
|
@@ -171,15 +297,26 @@ var validateKey = async (value, scopeId) => {
|
|
|
171
297
|
throw keyLost();
|
|
172
298
|
return Object.freeze({ privateKey: value.privateKey, publicJwk: Object.freeze({ ...jwk }), jkt: value.jkt, scopeId });
|
|
173
299
|
};
|
|
174
|
-
var
|
|
300
|
+
var readOrCreateKey = async (input, create, authority) => {
|
|
301
|
+
authority?.assertCurrent();
|
|
175
302
|
assertDpopBrowserFeatures();
|
|
176
303
|
const { id } = resolveDpopScope(input);
|
|
177
304
|
try {
|
|
178
|
-
const existing = await withDpopDatabase((db) =>
|
|
179
|
-
|
|
180
|
-
|
|
305
|
+
const existing = await withDpopDatabase((db) => {
|
|
306
|
+
authority?.assertCurrent();
|
|
307
|
+
return db.get("keys", id);
|
|
308
|
+
});
|
|
309
|
+
authority?.assertCurrent();
|
|
310
|
+
if (existing) {
|
|
311
|
+
const key2 = await validateKey(existing, id);
|
|
312
|
+
authority?.assertCurrent();
|
|
313
|
+
return key2;
|
|
314
|
+
}
|
|
315
|
+
if (!create) throw keyLost();
|
|
181
316
|
const pair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, false, ["sign", "verify"]);
|
|
317
|
+
authority?.assertCurrent();
|
|
182
318
|
const exported = await crypto.subtle.exportKey("jwk", pair.publicKey);
|
|
319
|
+
authority?.assertCurrent();
|
|
183
320
|
if (!exported.x || !exported.y) throw keyLost();
|
|
184
321
|
const publicJwk = { kty: "EC", crv: "P-256", x: exported.x, y: exported.y };
|
|
185
322
|
const candidate = {
|
|
@@ -189,15 +326,22 @@ var getOrCreateDpopKey = async (input, options = {}) => {
|
|
|
189
326
|
publicJwk,
|
|
190
327
|
jkt: await (0, import_jose.calculateJwkThumbprint)(publicJwk)
|
|
191
328
|
};
|
|
329
|
+
authority?.assertCurrent();
|
|
192
330
|
const winner = await withDpopDatabase(async (db) => {
|
|
331
|
+
authority?.assertCurrent();
|
|
193
332
|
const transaction = db.transaction("keys", "readwrite");
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
333
|
+
return guardDpopTransaction(transaction, authority, async () => {
|
|
334
|
+
const stored = await transaction.store.get(id);
|
|
335
|
+
authority?.assertCurrent();
|
|
336
|
+
if (!stored) await transaction.store.add(candidate, id);
|
|
337
|
+
return stored ?? candidate;
|
|
338
|
+
});
|
|
198
339
|
});
|
|
199
|
-
|
|
340
|
+
const key = await validateKey(winner, id);
|
|
341
|
+
authority?.assertCurrent();
|
|
342
|
+
return key;
|
|
200
343
|
} catch (error) {
|
|
344
|
+
authority?.assertCurrent();
|
|
201
345
|
if (error instanceof OidcVaultDpopClientError) throw error;
|
|
202
346
|
throw new OidcVaultDpopClientError(
|
|
203
347
|
"DPOP_KEY_STORAGE_UNAVAILABLE",
|
|
@@ -206,11 +350,56 @@ var getOrCreateDpopKey = async (input, options = {}) => {
|
|
|
206
350
|
);
|
|
207
351
|
}
|
|
208
352
|
};
|
|
353
|
+
var getOrCreateLoginDpopKey = (input, authority) => readOrCreateKey(input, true, authority);
|
|
354
|
+
var getOrCreateDpopKey = (input, options = {}) => readOrCreateKey(input, options.create !== false);
|
|
209
355
|
|
|
210
356
|
// src/dpop-proof.ts
|
|
211
357
|
var import_jose2 = require("jose");
|
|
358
|
+
|
|
359
|
+
// src/abort.ts
|
|
360
|
+
var bestEffort = (cleanup) => {
|
|
361
|
+
try {
|
|
362
|
+
void Promise.resolve(cleanup()).catch(() => {
|
|
363
|
+
});
|
|
364
|
+
} catch {
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
var cancelResponse = (response) => bestEffort(() => response.body?.cancel());
|
|
368
|
+
var assertNotAborted = (signal) => {
|
|
369
|
+
if (signal?.aborted) throw signal.reason;
|
|
370
|
+
};
|
|
371
|
+
var abortable = (signal, start, discard) => new Promise((resolve, reject) => {
|
|
372
|
+
const detach = () => signal?.removeEventListener("abort", onAbort);
|
|
373
|
+
const onAbort = () => {
|
|
374
|
+
detach();
|
|
375
|
+
reject(signal.reason);
|
|
376
|
+
};
|
|
377
|
+
if (signal?.aborted) {
|
|
378
|
+
reject(signal.reason);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
382
|
+
const failed = (error) => {
|
|
383
|
+
detach();
|
|
384
|
+
reject(signal?.aborted ? signal.reason : error);
|
|
385
|
+
};
|
|
386
|
+
try {
|
|
387
|
+
void Promise.resolve(start()).then((value) => {
|
|
388
|
+
detach();
|
|
389
|
+
if (signal?.aborted) {
|
|
390
|
+
if (discard) bestEffort(() => discard(value));
|
|
391
|
+
reject(signal.reason);
|
|
392
|
+
} else resolve(value);
|
|
393
|
+
}, failed);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
failed(error);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// src/dpop-proof.ts
|
|
212
400
|
var createDpopProof = async (key, input) => {
|
|
213
|
-
const { method, url, accessToken, nonce, now = Date.now } = input;
|
|
401
|
+
const { method, url, accessToken, nonce, now = Date.now, signal } = input;
|
|
402
|
+
assertNotAborted(signal);
|
|
214
403
|
if (!/^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(method) || method !== method.toUpperCase()) {
|
|
215
404
|
throw new OidcVaultDpopClientError("INVALID_DPOP_METHOD", "Use the exact uppercase request method.");
|
|
216
405
|
}
|
|
@@ -225,18 +414,24 @@ var createDpopProof = async (key, input) => {
|
|
|
225
414
|
if (!/^[\x21-\x7e]+$/.test(accessToken))
|
|
226
415
|
throw new OidcVaultDpopClientError("INVALID_DPOP_TOKEN", "The access token is invalid.");
|
|
227
416
|
ath = import_jose2.base64url.encode(
|
|
228
|
-
new Uint8Array(
|
|
417
|
+
new Uint8Array(
|
|
418
|
+
await abortable(signal, () => crypto.subtle.digest("SHA-256", new TextEncoder().encode(accessToken)))
|
|
419
|
+
)
|
|
229
420
|
);
|
|
230
421
|
}
|
|
422
|
+
assertNotAborted(signal);
|
|
231
423
|
const jti = import_jose2.base64url.encode(crypto.getRandomValues(new Uint8Array(16)));
|
|
232
|
-
return
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
424
|
+
return abortable(
|
|
425
|
+
signal,
|
|
426
|
+
() => new import_jose2.SignJWT({
|
|
427
|
+
htm: method,
|
|
428
|
+
htu: normalizeDpopTarget(url),
|
|
429
|
+
iat,
|
|
430
|
+
jti,
|
|
431
|
+
...ath === void 0 ? {} : { ath },
|
|
432
|
+
...nonce === void 0 ? {} : { nonce }
|
|
433
|
+
}).setProtectedHeader({ typ: "dpop+jwt", alg: "ES256", jwk: { ...key.publicJwk } }).sign(key.privateKey)
|
|
434
|
+
);
|
|
240
435
|
};
|
|
241
436
|
|
|
242
437
|
// src/auth-session.ts
|
|
@@ -249,59 +444,73 @@ var invalidCredentials = () => new OidcVaultDpopClientError(
|
|
|
249
444
|
"A matching DPoP JWT response is required. Sign in again.",
|
|
250
445
|
true
|
|
251
446
|
);
|
|
252
|
-
var
|
|
253
|
-
|
|
254
|
-
const { accessToken, tokenType, expiresIn, sessionId, user } = value;
|
|
255
|
-
if (typeof accessToken !== "string" || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(accessToken) || tokenType !== "DPoP" || typeof expiresIn !== "number" || !Number.isSafeInteger(expiresIn) || expiresIn < 0 || transport === "body" && (typeof sessionId !== "string" || sessionId.length === 0) || transport === "cookie" && "sessionId" in value)
|
|
256
|
-
throw invalidCredentials();
|
|
257
|
-
let expiration;
|
|
447
|
+
var isSafeNonnegativeInteger = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
448
|
+
var decodeBoundJwtExpiration = (accessToken, jkt) => {
|
|
258
449
|
try {
|
|
450
|
+
const parts = accessToken.split(".");
|
|
451
|
+
if (parts.length !== 3 || !parts.every((part) => /^[A-Za-z0-9_-]+$/.test(part) && import_jose3.base64url.encode(import_jose3.base64url.decode(part)) === part))
|
|
452
|
+
throw invalidCredentials();
|
|
259
453
|
const header = (0, import_jose3.decodeProtectedHeader)(accessToken);
|
|
260
454
|
const claims = (0, import_jose3.decodeJwt)(accessToken);
|
|
261
455
|
const cnf = claims.cnf;
|
|
262
|
-
if (
|
|
456
|
+
if (typeof header.alg !== "string" || header.alg.length === 0 || header.alg === "none" || header.b64 === false || typeof jkt !== "string" || jkt.length !== 43 || !/^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/.test(jkt) || typeof cnf !== "object" || cnf === null || Array.isArray(cnf) || Reflect.ownKeys(cnf).length !== 1 || !Object.hasOwn(cnf, "jkt") || !("jkt" in cnf) || cnf.jkt !== jkt || !isSafeNonnegativeInteger(claims.exp))
|
|
263
457
|
throw invalidCredentials();
|
|
264
|
-
expiration =
|
|
265
|
-
if (!
|
|
458
|
+
const expiration = claims.exp * 1e3;
|
|
459
|
+
if (!isSafeNonnegativeInteger(expiration)) throw invalidCredentials();
|
|
460
|
+
return expiration;
|
|
266
461
|
} catch {
|
|
267
462
|
throw invalidCredentials();
|
|
268
463
|
}
|
|
464
|
+
};
|
|
465
|
+
var createToken = (accessToken, expiresAt, jkt, generation, user) => {
|
|
269
466
|
if (user !== void 0 && (typeof user !== "object" || user === null || Array.isArray(user) || !("sub" in user) || typeof user.sub !== "string"))
|
|
270
467
|
throw invalidCredentials();
|
|
271
|
-
|
|
468
|
+
return Object.freeze({
|
|
272
469
|
accessToken,
|
|
273
|
-
tokenType,
|
|
274
|
-
expiresAt
|
|
470
|
+
tokenType: "DPoP",
|
|
471
|
+
expiresAt,
|
|
275
472
|
jkt,
|
|
276
|
-
generation
|
|
473
|
+
generation,
|
|
277
474
|
...user === void 0 ? {} : { user: structuredClone(user) }
|
|
278
475
|
});
|
|
476
|
+
};
|
|
477
|
+
var parseDpopCredentials = (value, transport, jkt, now) => {
|
|
478
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw invalidCredentials();
|
|
479
|
+
const { accessToken, tokenType, expiresIn, sessionId, user } = value;
|
|
480
|
+
if (typeof accessToken !== "string" || tokenType !== "DPoP" || !isSafeNonnegativeInteger(expiresIn) || !isSafeNonnegativeInteger(now) || transport === "body" && (typeof sessionId !== "string" || sessionId.length === 0) || transport === "cookie" && "sessionId" in value)
|
|
481
|
+
throw invalidCredentials();
|
|
482
|
+
const lifetimeMs = expiresIn * 1e3;
|
|
483
|
+
if (!isSafeNonnegativeInteger(lifetimeMs)) throw invalidCredentials();
|
|
484
|
+
const responseDeadline = now + lifetimeMs;
|
|
485
|
+
if (!isSafeNonnegativeInteger(responseDeadline)) throw invalidCredentials();
|
|
486
|
+
const expiration = Math.min(responseDeadline, decodeBoundJwtExpiration(accessToken, jkt));
|
|
487
|
+
const token = createToken(accessToken, expiration, jkt, crypto.randomUUID(), user);
|
|
279
488
|
return { token, ...transport === "body" ? { sessionId } : {} };
|
|
280
489
|
};
|
|
281
490
|
var readPeerToken = (value, jkt, generation, now) => {
|
|
282
|
-
if (typeof value !== "object" || value === null)
|
|
283
|
-
const candidate = value;
|
|
284
|
-
if (candidate.jkt !== jkt || candidate.generation !== generation || candidate.tokenType !== "DPoP" || !Number.isSafeInteger(candidate.expiresAt) || candidate.expiresAt <= now)
|
|
491
|
+
if (!isSafeNonnegativeInteger(now) || typeof value !== "object" || value === null || Array.isArray(value) || "sessionId" in value)
|
|
285
492
|
return void 0;
|
|
286
493
|
try {
|
|
287
|
-
const {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
);
|
|
298
|
-
|
|
494
|
+
const {
|
|
495
|
+
accessToken,
|
|
496
|
+
tokenType,
|
|
497
|
+
expiresAt,
|
|
498
|
+
jkt: peerJkt,
|
|
499
|
+
generation: peerGeneration,
|
|
500
|
+
user
|
|
501
|
+
} = value;
|
|
502
|
+
if (typeof accessToken !== "string" || peerJkt !== jkt || peerGeneration !== generation || tokenType !== "DPoP" || !isSafeNonnegativeInteger(expiresAt) || expiresAt <= now)
|
|
503
|
+
return void 0;
|
|
504
|
+
const expiration = Math.min(expiresAt, decodeBoundJwtExpiration(accessToken, jkt));
|
|
505
|
+
if (expiration <= now) return void 0;
|
|
506
|
+
return createToken(accessToken, expiration, jkt, generation, user);
|
|
299
507
|
} catch {
|
|
300
508
|
return void 0;
|
|
301
509
|
}
|
|
302
510
|
};
|
|
303
511
|
|
|
304
512
|
// src/nonce-cache.ts
|
|
513
|
+
var MAX_ENTRIES = 64;
|
|
305
514
|
var DpopNonceCache = class {
|
|
306
515
|
constructor() {
|
|
307
516
|
this.nonces = /* @__PURE__ */ new Map();
|
|
@@ -309,16 +518,147 @@ var DpopNonceCache = class {
|
|
|
309
518
|
get(space, jkt) {
|
|
310
519
|
return this.nonces.get(JSON.stringify([space, jkt]));
|
|
311
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* Accept one 1..512-character visible-ASCII value, excluding whitespace,
|
|
523
|
+
* double quotes, backslashes and commas (including coalesced duplicate header
|
|
524
|
+
* values). Invalid input returns false without overwriting or changing order.
|
|
525
|
+
*/
|
|
312
526
|
remember(space, jkt, nonce) {
|
|
313
|
-
if (
|
|
314
|
-
|
|
527
|
+
if (typeof nonce !== "string" || nonce.length > 512 || !/^[\x21\x23-\x2b\x2d-\x5b\x5d-\x7e]+$/.test(nonce))
|
|
528
|
+
return false;
|
|
529
|
+
const id = JSON.stringify([space, jkt]);
|
|
530
|
+
this.nonces.delete(id);
|
|
531
|
+
if (this.nonces.size === MAX_ENTRIES) this.nonces.delete(this.nonces.keys().next().value);
|
|
532
|
+
this.nonces.set(id, nonce);
|
|
315
533
|
return true;
|
|
316
534
|
}
|
|
535
|
+
/** Forget every tuple in this cache; does not invalidate credentials or server nonce state. */
|
|
317
536
|
clear() {
|
|
318
537
|
this.nonces.clear();
|
|
319
538
|
}
|
|
320
539
|
};
|
|
321
540
|
|
|
541
|
+
// src/session-lifecycle.ts
|
|
542
|
+
var superseded = () => new OidcVaultDpopClientError("DPOP_OPERATION_SUPERSEDED", "This browser auth operation was superseded.");
|
|
543
|
+
var closed = () => new OidcVaultDpopClientError("DPOP_SESSION_DISPOSED", "This browser auth context is closed.");
|
|
544
|
+
var createSessionLifecycle = () => {
|
|
545
|
+
let disposed = false;
|
|
546
|
+
let controller = new AbortController();
|
|
547
|
+
let tail = Promise.resolve();
|
|
548
|
+
const assertLive = () => {
|
|
549
|
+
if (disposed) throw closed();
|
|
550
|
+
};
|
|
551
|
+
const capture = (current = () => true) => {
|
|
552
|
+
assertLive();
|
|
553
|
+
const { signal } = controller;
|
|
554
|
+
return {
|
|
555
|
+
signal,
|
|
556
|
+
isCurrent: () => !disposed && !signal.aborted && current(),
|
|
557
|
+
assertCurrent() {
|
|
558
|
+
assertLive();
|
|
559
|
+
if (signal.aborted || !current()) throw superseded();
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
};
|
|
563
|
+
return {
|
|
564
|
+
capture,
|
|
565
|
+
isLive: () => !disposed,
|
|
566
|
+
invalidate() {
|
|
567
|
+
assertLive();
|
|
568
|
+
const previous = controller;
|
|
569
|
+
controller = new AbortController();
|
|
570
|
+
previous.abort(superseded());
|
|
571
|
+
return capture();
|
|
572
|
+
},
|
|
573
|
+
enqueue(authority, run) {
|
|
574
|
+
const pending = tail.then(() => {
|
|
575
|
+
authority.assertCurrent();
|
|
576
|
+
return run();
|
|
577
|
+
});
|
|
578
|
+
tail = pending.then(
|
|
579
|
+
() => {
|
|
580
|
+
},
|
|
581
|
+
() => {
|
|
582
|
+
}
|
|
583
|
+
);
|
|
584
|
+
return pending;
|
|
585
|
+
},
|
|
586
|
+
dispose() {
|
|
587
|
+
if (disposed) return;
|
|
588
|
+
disposed = true;
|
|
589
|
+
controller.abort(closed());
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
// src/vault-http.ts
|
|
595
|
+
var requestVaultJson = async (request, url, init, { signal, requestTimeoutMs, maxResponseBytes }) => {
|
|
596
|
+
assertNotAborted(signal);
|
|
597
|
+
const controller = new AbortController();
|
|
598
|
+
const onAbort = () => controller.abort(signal.reason);
|
|
599
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
600
|
+
const started = performance.now();
|
|
601
|
+
const timeout = setTimeout(() => controller.abort(vaultRequestTimeout()), requestTimeoutMs);
|
|
602
|
+
const assertActive = () => {
|
|
603
|
+
if (!controller.signal.aborted && performance.now() - started >= requestTimeoutMs)
|
|
604
|
+
controller.abort(vaultRequestTimeout());
|
|
605
|
+
assertNotAborted(controller.signal);
|
|
606
|
+
};
|
|
607
|
+
let response;
|
|
608
|
+
let reader;
|
|
609
|
+
let bodyDone = false;
|
|
610
|
+
try {
|
|
611
|
+
response = await abortable(
|
|
612
|
+
controller.signal,
|
|
613
|
+
() => request(url, { ...init, signal: controller.signal }),
|
|
614
|
+
cancelResponse
|
|
615
|
+
);
|
|
616
|
+
assertActive();
|
|
617
|
+
const status = response.status;
|
|
618
|
+
const nonce = response.headers.get("DPoP-Nonce");
|
|
619
|
+
let value;
|
|
620
|
+
try {
|
|
621
|
+
if (response.body) {
|
|
622
|
+
const bodyReader = response.body.getReader();
|
|
623
|
+
reader = bodyReader;
|
|
624
|
+
const decoder = new TextDecoder();
|
|
625
|
+
let bytes = 0;
|
|
626
|
+
let text = "";
|
|
627
|
+
for (; ; ) {
|
|
628
|
+
assertActive();
|
|
629
|
+
const chunk = await abortable(controller.signal, () => bodyReader.read());
|
|
630
|
+
assertActive();
|
|
631
|
+
if (chunk.done) {
|
|
632
|
+
bodyDone = true;
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
if (!ArrayBuffer.isView(chunk.value)) throw new TypeError("Invalid vault response body.");
|
|
636
|
+
if (chunk.value.byteLength > maxResponseBytes - bytes) {
|
|
637
|
+
controller.abort(vaultResponseTooLarge());
|
|
638
|
+
assertActive();
|
|
639
|
+
}
|
|
640
|
+
bytes += chunk.value.byteLength;
|
|
641
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
642
|
+
}
|
|
643
|
+
value = JSON.parse(text + decoder.decode());
|
|
644
|
+
}
|
|
645
|
+
} catch {
|
|
646
|
+
assertActive();
|
|
647
|
+
value = void 0;
|
|
648
|
+
}
|
|
649
|
+
assertActive();
|
|
650
|
+
return { status, nonce, value };
|
|
651
|
+
} finally {
|
|
652
|
+
clearTimeout(timeout);
|
|
653
|
+
signal.removeEventListener("abort", onAbort);
|
|
654
|
+
if (reader) {
|
|
655
|
+
const bodyReader = reader;
|
|
656
|
+
if (!bodyDone) bestEffort(() => bodyReader.cancel());
|
|
657
|
+
bestEffort(() => bodyReader.releaseLock());
|
|
658
|
+
} else if (response) cancelResponse(response);
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
|
|
322
662
|
// src/auth-session.ts
|
|
323
663
|
var createOidcVaultDpopSession = (options) => {
|
|
324
664
|
const {
|
|
@@ -326,11 +666,17 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
326
666
|
basePath = "/auth/oidc",
|
|
327
667
|
sessionTransport = "body",
|
|
328
668
|
fingerprint,
|
|
669
|
+
requestTimeoutMs = 15e3,
|
|
670
|
+
maxResponseBytes = 1048576,
|
|
329
671
|
fetch: request = globalThis.fetch.bind(globalThis),
|
|
330
672
|
navigate = (url) => location.assign(url),
|
|
331
673
|
now = Date.now
|
|
332
674
|
} = options;
|
|
333
675
|
if (sessionTransport !== "body" && sessionTransport !== "cookie") throw new TypeError("Unknown session transport.");
|
|
676
|
+
if (!Number.isSafeInteger(requestTimeoutMs) || requestTimeoutMs < 1 || requestTimeoutMs > 2147483647)
|
|
677
|
+
throw new TypeError("requestTimeoutMs must be an integer from 1 to 2147483647 milliseconds.");
|
|
678
|
+
if (!Number.isSafeInteger(maxResponseBytes) || maxResponseBytes < 1)
|
|
679
|
+
throw new TypeError("maxResponseBytes must be a positive safe integer.");
|
|
334
680
|
assertDpopBrowserFeatures(sessionTransport === "cookie");
|
|
335
681
|
const scope = resolveDpopScope({ frontendOrigin: location.origin, backendOrigin, basePath });
|
|
336
682
|
const storageKey = `oidc-vault-dpop:handle:${scope.id}`;
|
|
@@ -341,9 +687,13 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
341
687
|
const channel = sessionTransport === "cookie" ? new BroadcastChannel(lockName) : void 0;
|
|
342
688
|
let token;
|
|
343
689
|
let tokenRecognition;
|
|
690
|
+
const cookieBindings = /* @__PURE__ */ new WeakMap();
|
|
344
691
|
let refreshPromise;
|
|
345
|
-
let
|
|
346
|
-
|
|
692
|
+
let resetPromise;
|
|
693
|
+
const lifecycle = createSessionLifecycle();
|
|
694
|
+
let publication = 0;
|
|
695
|
+
let loginIntent = 0;
|
|
696
|
+
let acceptPeerTokens = true;
|
|
347
697
|
const peerWaiters = /* @__PURE__ */ new Set();
|
|
348
698
|
const readStorage = (key) => {
|
|
349
699
|
try {
|
|
@@ -373,35 +723,65 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
373
723
|
const clearLocal = (clearPendingLogin = true) => {
|
|
374
724
|
token = void 0;
|
|
375
725
|
tokenRecognition = void 0;
|
|
376
|
-
|
|
726
|
+
acceptPeerTokens = false;
|
|
727
|
+
publication++;
|
|
728
|
+
for (const waiter of peerWaiters) waiter.finish();
|
|
377
729
|
nonces.clear();
|
|
378
730
|
store(storageKey);
|
|
379
731
|
if (clearPendingLogin) store(pendingKey);
|
|
380
732
|
};
|
|
381
|
-
const
|
|
382
|
-
|
|
733
|
+
const invalidateLocal = (clearPendingLogin = true) => {
|
|
734
|
+
const authority = lifecycle.invalidate();
|
|
735
|
+
refreshPromise = void 0;
|
|
736
|
+
resetPromise = void 0;
|
|
737
|
+
clearLocal(clearPendingLogin);
|
|
738
|
+
return authority;
|
|
383
739
|
};
|
|
384
740
|
const withCookieLock = (operation2) => sessionTransport === "cookie" ? navigator.locks.request(lockName, { mode: "exclusive" }, operation2) : operation2();
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
clearLocal();
|
|
741
|
+
const invalidateCookie = async (authority, oldJkt, expected) => {
|
|
742
|
+
authority.assertCurrent();
|
|
388
743
|
if (channel) {
|
|
389
|
-
|
|
744
|
+
let previous;
|
|
745
|
+
if (expected !== void 0) {
|
|
746
|
+
const current = await readDpopScopeState(scope.id, authority);
|
|
747
|
+
authority.assertCurrent();
|
|
748
|
+
if (!expected || !current.keyPresent && !current.cookiePresent || current.keyJkt !== void 0 && current.keyJkt !== expected.jkt || current.version && (expected.generation !== void 0 && current.version.generation !== expected.generation || current.version.active && current.version.jkt !== expected.jkt))
|
|
749
|
+
return;
|
|
750
|
+
previous = current.version;
|
|
751
|
+
} else previous = await readCookieSessionVersion(scope.id);
|
|
752
|
+
authority.assertCurrent();
|
|
390
753
|
const jkt = oldJkt ?? previous?.jkt ?? "";
|
|
391
754
|
const generation = crypto.randomUUID();
|
|
392
|
-
|
|
755
|
+
const next = { jkt, generation, active: false };
|
|
756
|
+
await writeCookieSessionVersion(scope.id, next, authority);
|
|
757
|
+
authority.assertCurrent();
|
|
758
|
+
cookieBindings.set(authority, next);
|
|
393
759
|
channel.postMessage({ type: "clear", sender, jkt, generation });
|
|
394
760
|
}
|
|
395
761
|
};
|
|
396
|
-
const existingKey = async (expectedJkt) => {
|
|
397
|
-
|
|
762
|
+
const existingKey = async (authority, expectedJkt) => {
|
|
763
|
+
authority.assertCurrent();
|
|
764
|
+
if (channel && expectedJkt !== void 0)
|
|
765
|
+
cookieBindings.set(authority, {
|
|
766
|
+
...cookieBindings.get(authority),
|
|
767
|
+
jkt: expectedJkt
|
|
768
|
+
});
|
|
398
769
|
const key = await getOrCreateDpopKey(scope, { create: false });
|
|
770
|
+
authority.assertCurrent();
|
|
399
771
|
if (expectedJkt !== void 0 && expectedJkt !== key.jkt) throw keyLost();
|
|
400
772
|
return key;
|
|
401
773
|
};
|
|
402
|
-
const authenticatedKey = async () => {
|
|
403
|
-
|
|
404
|
-
|
|
774
|
+
const authenticatedKey = async (authority) => {
|
|
775
|
+
authority.assertCurrent();
|
|
776
|
+
let version;
|
|
777
|
+
if (!token && channel) {
|
|
778
|
+
version = await readCookieSessionVersion(scope.id);
|
|
779
|
+
authority.assertCurrent();
|
|
780
|
+
if (version) cookieBindings.set(authority, { ...version });
|
|
781
|
+
}
|
|
782
|
+
const expected = token?.jkt ?? (sessionTransport === "body" ? readStorage(storageKey)?.jkt : version?.jkt);
|
|
783
|
+
const key = await existingKey(authority, expected);
|
|
784
|
+
authority.assertCurrent();
|
|
405
785
|
if (!expected) throw loginRequired();
|
|
406
786
|
return key;
|
|
407
787
|
};
|
|
@@ -417,49 +797,83 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
417
797
|
)
|
|
418
798
|
)
|
|
419
799
|
);
|
|
420
|
-
const currentRecognition = async () =>
|
|
421
|
-
|
|
800
|
+
const currentRecognition = async (authority) => {
|
|
801
|
+
authority.assertCurrent();
|
|
802
|
+
const headers = await fingerprint?.headers() ?? {};
|
|
803
|
+
authority.assertCurrent();
|
|
804
|
+
const marker = await recognitionMarker(headers);
|
|
805
|
+
authority.assertCurrent();
|
|
806
|
+
return marker;
|
|
807
|
+
};
|
|
808
|
+
const post = async (authority, route, body, key) => {
|
|
422
809
|
const url = `${scope.backendOrigin}${scope.basePath === "/" ? "" : scope.basePath}/${route}`;
|
|
423
|
-
|
|
810
|
+
const { jkt } = key;
|
|
811
|
+
let nonceRetried = false;
|
|
812
|
+
for (; ; ) {
|
|
813
|
+
authority.assertCurrent();
|
|
424
814
|
const recognition = route === "logout" ? {} : await fingerprint?.headers() ?? {};
|
|
425
|
-
const currentKey = await existingKey(
|
|
815
|
+
const currentKey = await existingKey(authority, jkt);
|
|
816
|
+
authority.assertCurrent();
|
|
426
817
|
const proof = await createDpopProof(currentKey, {
|
|
427
818
|
method: "POST",
|
|
428
819
|
url,
|
|
429
820
|
now,
|
|
430
|
-
nonce: nonces.get(scope.id,
|
|
431
|
-
|
|
432
|
-
const response = await request(url, {
|
|
433
|
-
method: "POST",
|
|
434
|
-
credentials: route === "login" || route === "exchange" || sessionTransport === "cookie" ? "include" : "omit",
|
|
435
|
-
redirect: "error",
|
|
436
|
-
cache: "no-store",
|
|
437
|
-
headers: { "Content-Type": "application/json", ...recognition, DPoP: proof },
|
|
438
|
-
body: JSON.stringify(body)
|
|
821
|
+
nonce: nonces.get(scope.id, jkt),
|
|
822
|
+
signal: authority.signal
|
|
439
823
|
});
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
824
|
+
authority.assertCurrent();
|
|
825
|
+
const { status, nonce, value } = await requestVaultJson(
|
|
826
|
+
request,
|
|
827
|
+
url,
|
|
828
|
+
{
|
|
829
|
+
method: "POST",
|
|
830
|
+
credentials: route === "login" || route === "exchange" || sessionTransport === "cookie" ? "include" : "omit",
|
|
831
|
+
redirect: "error",
|
|
832
|
+
cache: "no-store",
|
|
833
|
+
headers: { "Content-Type": "application/json", ...recognition, DPoP: proof },
|
|
834
|
+
body: JSON.stringify(body)
|
|
835
|
+
},
|
|
836
|
+
{ signal: authority.signal, requestTimeoutMs, maxResponseBytes }
|
|
837
|
+
);
|
|
838
|
+
authority.assertCurrent();
|
|
839
|
+
const learnedNonce = nonces.remember(scope.id, jkt, nonce);
|
|
840
|
+
if (status >= 200 && status < 300) {
|
|
841
|
+
const marker = await recognitionMarker(recognition);
|
|
842
|
+
authority.assertCurrent();
|
|
843
|
+
return { value, recognition: marker };
|
|
844
|
+
}
|
|
845
|
+
const error = serverError(status, value);
|
|
846
|
+
if (error.code === "OIDC_VAULT_USE_DPOP_NONCE" && (status === 400 || status === 401)) {
|
|
847
|
+
if (nonceRetried)
|
|
848
|
+
throw new OidcVaultDpopClientError(
|
|
849
|
+
"DPOP_NONCE_RETRY_EXHAUSTED",
|
|
850
|
+
"The DPoP nonce challenge was repeated.",
|
|
851
|
+
false,
|
|
852
|
+
status
|
|
853
|
+
);
|
|
854
|
+
if (learnedNonce) {
|
|
855
|
+
nonceRetried = true;
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
445
858
|
}
|
|
446
|
-
if (response.ok) return { value, recognition: await recognitionMarker(recognition) };
|
|
447
|
-
const error = serverError(response.status, value);
|
|
448
|
-
if (error.code === "OIDC_VAULT_USE_DPOP_NONCE" && (response.status === 400 || response.status === 401) && attempt === 0 && nonces.remember(scope.id, key.jkt, response.headers.get("DPoP-Nonce")))
|
|
449
|
-
continue;
|
|
450
859
|
throw error;
|
|
451
860
|
}
|
|
452
|
-
throw new OidcVaultDpopClientError("DPOP_NONCE_RETRY_EXHAUSTED", "The DPoP nonce challenge was repeated.");
|
|
453
861
|
};
|
|
454
|
-
const publish = async (next, recognition,
|
|
455
|
-
await existingKey(next.jkt);
|
|
456
|
-
|
|
862
|
+
const publish = async (next, recognition, authority, sessionId) => {
|
|
863
|
+
await existingKey(authority, next.jkt);
|
|
864
|
+
authority.assertCurrent();
|
|
457
865
|
if (channel) {
|
|
458
|
-
|
|
866
|
+
const version = { jkt: next.jkt, generation: next.generation, active: true };
|
|
867
|
+
await writeCookieSessionVersion(scope.id, version, authority);
|
|
868
|
+
authority.assertCurrent();
|
|
869
|
+
cookieBindings.set(authority, version);
|
|
459
870
|
}
|
|
460
|
-
|
|
871
|
+
authority.assertCurrent();
|
|
872
|
+
if (sessionTransport === "body") store(storageKey, { sessionId, jkt: next.jkt });
|
|
461
873
|
token = next;
|
|
462
874
|
tokenRecognition = recognition;
|
|
875
|
+
acceptPeerTokens = true;
|
|
876
|
+
publication++;
|
|
463
877
|
channel?.postMessage({
|
|
464
878
|
type: "token",
|
|
465
879
|
sender,
|
|
@@ -470,52 +884,101 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
470
884
|
});
|
|
471
885
|
return next;
|
|
472
886
|
};
|
|
473
|
-
const acceptResponse = async (response, key,
|
|
474
|
-
|
|
887
|
+
const acceptResponse = async (response, key, authority) => {
|
|
888
|
+
authority.assertCurrent();
|
|
475
889
|
const parsed = parseDpopCredentials(response.value, sessionTransport, key.jkt, now());
|
|
476
|
-
|
|
477
|
-
|
|
890
|
+
return publish(parsed.token, response.recognition, authority, parsed.sessionId);
|
|
891
|
+
};
|
|
892
|
+
const stateAuthority = (current = () => true) => {
|
|
893
|
+
const captured = publication;
|
|
894
|
+
return lifecycle.capture(() => publication === captured && current());
|
|
895
|
+
};
|
|
896
|
+
const peerAuthority = (current = () => true) => {
|
|
897
|
+
const intent = loginIntent;
|
|
898
|
+
return stateAuthority(() => loginIntent === intent && current());
|
|
478
899
|
};
|
|
479
|
-
const
|
|
480
|
-
if (
|
|
481
|
-
|
|
482
|
-
|
|
900
|
+
const onPeerKeyFailure = (error, authority, jkt) => {
|
|
901
|
+
if (!authority.isCurrent() || token?.jkt !== jkt || !(error instanceof OidcVaultDpopClientError) || !error.requiresLogin)
|
|
902
|
+
return;
|
|
903
|
+
try {
|
|
904
|
+
invalidateLocal(false);
|
|
905
|
+
} catch {
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
const adoptPeer = async (message, authority, expectedRecognition) => {
|
|
909
|
+
authority.assertCurrent();
|
|
910
|
+
const unsolicited = expectedRecognition === void 0;
|
|
911
|
+
if (message.type !== "token" || !acceptPeerTokens && unsolicited) return void 0;
|
|
912
|
+
const recognition = expectedRecognition ?? await currentRecognition(authority);
|
|
913
|
+
authority.assertCurrent();
|
|
483
914
|
if (message.recognition !== recognition) return void 0;
|
|
484
915
|
const current = await readCookieSessionVersion(scope.id);
|
|
916
|
+
authority.assertCurrent();
|
|
485
917
|
if (!current?.active || current.jkt !== message.jkt || current.generation !== message.generation) return void 0;
|
|
486
918
|
const peer = readPeerToken(message.token, current.jkt, current.generation, now());
|
|
487
919
|
if (!peer) return void 0;
|
|
488
|
-
|
|
920
|
+
try {
|
|
921
|
+
await existingKey(authority, current.jkt);
|
|
922
|
+
} catch (error) {
|
|
923
|
+
if (unsolicited) onPeerKeyFailure(error, authority, current.jkt);
|
|
924
|
+
throw error;
|
|
925
|
+
}
|
|
926
|
+
authority.assertCurrent();
|
|
489
927
|
const latest = await readCookieSessionVersion(scope.id);
|
|
490
|
-
|
|
928
|
+
authority.assertCurrent();
|
|
929
|
+
if (!latest?.active || latest.jkt !== current.jkt || latest.generation !== current.generation || peer.expiresAt <= now())
|
|
491
930
|
return void 0;
|
|
931
|
+
if (token?.generation === peer.generation && token.accessToken === peer.accessToken && token.expiresAt === peer.expiresAt && tokenRecognition === recognition)
|
|
932
|
+
return token;
|
|
492
933
|
token = peer;
|
|
493
934
|
tokenRecognition = recognition;
|
|
935
|
+
acceptPeerTokens = true;
|
|
936
|
+
publication++;
|
|
494
937
|
return peer;
|
|
495
938
|
};
|
|
496
|
-
const requestPeer = (jkt, generation, recognition, rejectedToken) => new Promise((resolve) => {
|
|
939
|
+
const requestPeer = (authority, jkt, generation, recognition, rejectedToken) => new Promise((resolve, reject) => {
|
|
940
|
+
authority.assertCurrent();
|
|
497
941
|
let settled = false;
|
|
942
|
+
let adopting = false;
|
|
498
943
|
const finish = (value) => {
|
|
499
944
|
if (settled) return;
|
|
500
945
|
settled = true;
|
|
501
946
|
clearTimeout(timeout);
|
|
947
|
+
authority.signal.removeEventListener("abort", onInvalidation);
|
|
502
948
|
peerWaiters.delete(waiter);
|
|
503
949
|
resolve(value);
|
|
504
950
|
};
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
951
|
+
const onInvalidation = () => finish();
|
|
952
|
+
const waiter = {
|
|
953
|
+
finish: () => finish(),
|
|
954
|
+
receive(message) {
|
|
955
|
+
if (settled || adopting || message.jkt !== jkt || message.generation !== generation) return;
|
|
956
|
+
adopting = true;
|
|
957
|
+
void adoptPeer(
|
|
958
|
+
message,
|
|
959
|
+
peerAuthority(() => !settled && authority.isCurrent()),
|
|
960
|
+
recognition
|
|
961
|
+
).then((peer) => {
|
|
962
|
+
adopting = false;
|
|
963
|
+
if (peer && peer.accessToken !== rejectedToken) finish(peer);
|
|
964
|
+
}).catch(() => finish());
|
|
965
|
+
}
|
|
510
966
|
};
|
|
511
967
|
const timeout = setTimeout(() => finish(), 180);
|
|
512
968
|
peerWaiters.add(waiter);
|
|
513
|
-
|
|
969
|
+
authority.signal.addEventListener("abort", onInvalidation, { once: true });
|
|
970
|
+
try {
|
|
971
|
+
channel?.postMessage({ type: "request-token", sender, jkt, generation, recognition });
|
|
972
|
+
} catch (error) {
|
|
973
|
+
reject(error);
|
|
974
|
+
finish();
|
|
975
|
+
}
|
|
514
976
|
});
|
|
515
977
|
if (channel)
|
|
516
978
|
channel.onmessage = (event) => {
|
|
979
|
+
if (!lifecycle.isLive()) return;
|
|
517
980
|
const message = event.data;
|
|
518
|
-
if (!message || message.sender === sender || typeof message.sender !== "string" || typeof message.jkt !== "string" || typeof message.generation !== "string" || !["token", "request-token", "clear"].includes(message.type ?? ""))
|
|
981
|
+
if (!message || message.sender === sender || typeof message.sender !== "string" || typeof message.jkt !== "string" || typeof message.generation !== "string" || message.keyJkt !== void 0 && typeof message.keyJkt !== "string" || !["token", "request-token", "clear", "reset-key"].includes(message.type ?? ""))
|
|
519
982
|
return;
|
|
520
983
|
const captured = message;
|
|
521
984
|
if (captured.type === "request-token") {
|
|
@@ -530,38 +993,83 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
530
993
|
recognition: tokenRecognition
|
|
531
994
|
});
|
|
532
995
|
}
|
|
533
|
-
} else if (captured.type === "clear") {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
996
|
+
} else if (captured.type === "clear" || captured.type === "reset-key") {
|
|
997
|
+
const authority = peerAuthority();
|
|
998
|
+
void lifecycle.enqueue(
|
|
999
|
+
authority,
|
|
1000
|
+
() => withCookieLock(async () => {
|
|
1001
|
+
authority.assertCurrent();
|
|
1002
|
+
if (captured.type === "clear") {
|
|
1003
|
+
const version = await readCookieSessionVersion(scope.id);
|
|
1004
|
+
authority.assertCurrent();
|
|
1005
|
+
if (!version?.active && version?.generation === captured.generation && version.jkt === captured.jkt)
|
|
1006
|
+
invalidateLocal(false);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const current = await readDpopScopeState(scope.id, authority);
|
|
1010
|
+
authority.assertCurrent();
|
|
1011
|
+
if (current.keyPresent || current.cookiePresent) return;
|
|
1012
|
+
const matchesKey = (jkt) => jkt === captured.jkt || jkt === captured.keyJkt;
|
|
1013
|
+
const pending = readStorage(pendingKey);
|
|
1014
|
+
if (pending && !matchesKey(pending.jkt)) return;
|
|
1015
|
+
if (token && !matchesKey(token.jkt) && token.generation !== captured.generation) return;
|
|
1016
|
+
invalidateLocal();
|
|
1017
|
+
})
|
|
1018
|
+
).catch(() => {
|
|
1019
|
+
});
|
|
538
1020
|
} else {
|
|
539
|
-
for (const waiter of peerWaiters) waiter(captured);
|
|
540
|
-
|
|
1021
|
+
if (peerWaiters.size) for (const waiter of peerWaiters) waiter.receive(captured);
|
|
1022
|
+
else {
|
|
1023
|
+
const authority = peerAuthority();
|
|
1024
|
+
void adoptPeer(captured, authority).catch(() => {
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
541
1027
|
}
|
|
542
1028
|
};
|
|
543
|
-
const onFailure = async (error) => {
|
|
544
|
-
|
|
1029
|
+
const onFailure = async (error, authority) => {
|
|
1030
|
+
authority.assertCurrent();
|
|
1031
|
+
if (error instanceof OidcVaultDpopClientError && error.requiresLogin) {
|
|
1032
|
+
const oldJkt = token?.jkt;
|
|
1033
|
+
const expected = token ? { jkt: token.jkt, generation: token.generation } : cookieBindings.get(authority) ?? null;
|
|
1034
|
+
const invalidation = invalidateLocal();
|
|
1035
|
+
await invalidateCookie(invalidation, oldJkt, expected);
|
|
1036
|
+
invalidation.assertCurrent();
|
|
1037
|
+
}
|
|
545
1038
|
throw error;
|
|
546
1039
|
};
|
|
547
|
-
const operation = (run) =>
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
1040
|
+
const operation = (authority, run) => lifecycle.enqueue(
|
|
1041
|
+
authority,
|
|
1042
|
+
() => withCookieLock(async () => {
|
|
1043
|
+
authority.assertCurrent();
|
|
1044
|
+
try {
|
|
1045
|
+
const result = await run();
|
|
1046
|
+
authority.assertCurrent();
|
|
1047
|
+
return result;
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
return onFailure(error, authority);
|
|
1050
|
+
}
|
|
1051
|
+
})
|
|
1052
|
+
);
|
|
554
1053
|
const session = {
|
|
555
1054
|
scopeId: scope.id,
|
|
556
1055
|
sessionTransport,
|
|
557
1056
|
async login(returnTo) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
1057
|
+
const authority = lifecycle.capture();
|
|
1058
|
+
loginIntent++;
|
|
1059
|
+
refreshPromise = void 0;
|
|
1060
|
+
resetPromise = void 0;
|
|
1061
|
+
await operation(authority, async () => {
|
|
1062
|
+
const oldJkt = token?.jkt;
|
|
1063
|
+
clearLocal();
|
|
1064
|
+
await invalidateCookie(authority, oldJkt);
|
|
1065
|
+
authority.assertCurrent();
|
|
1066
|
+
const key = await getOrCreateLoginDpopKey(scope, authority);
|
|
1067
|
+
authority.assertCurrent();
|
|
562
1068
|
store(pendingKey, { jkt: key.jkt });
|
|
1069
|
+
publication++;
|
|
563
1070
|
const body = returnTo === void 0 ? {} : { returnTo };
|
|
564
|
-
const { value } = await post("login", body, key);
|
|
1071
|
+
const { value } = await post(authority, "login", body, key);
|
|
1072
|
+
authority.assertCurrent();
|
|
565
1073
|
if (typeof value !== "object" || value === null || !("authorizationUrl" in value) || typeof value.authorizationUrl !== "string") {
|
|
566
1074
|
throw new OidcVaultDpopClientError(
|
|
567
1075
|
"INVALID_LOGIN_RESPONSE",
|
|
@@ -586,56 +1094,69 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
586
1094
|
true
|
|
587
1095
|
);
|
|
588
1096
|
}
|
|
1097
|
+
authority.assertCurrent();
|
|
589
1098
|
navigate(url.href);
|
|
590
1099
|
});
|
|
591
1100
|
},
|
|
592
1101
|
async exchange(code) {
|
|
593
|
-
|
|
594
|
-
|
|
1102
|
+
const authority = lifecycle.capture();
|
|
1103
|
+
refreshPromise = void 0;
|
|
1104
|
+
resetPromise = void 0;
|
|
1105
|
+
return operation(authority, async () => {
|
|
595
1106
|
const pending = readStorage(pendingKey);
|
|
596
|
-
const key = await existingKey(pending?.jkt);
|
|
1107
|
+
const key = await existingKey(authority, pending?.jkt);
|
|
1108
|
+
authority.assertCurrent();
|
|
597
1109
|
if (!pending?.jkt || typeof code !== "string" || code.length === 0) throw loginRequired();
|
|
598
|
-
const
|
|
599
|
-
|
|
1110
|
+
const result = await acceptResponse(await post(authority, "exchange", { code }, key), key, authority);
|
|
1111
|
+
authority.assertCurrent();
|
|
600
1112
|
store(pendingKey);
|
|
601
1113
|
return result;
|
|
602
1114
|
});
|
|
603
1115
|
},
|
|
604
1116
|
refresh(refreshOptions = {}) {
|
|
605
|
-
|
|
1117
|
+
const authority = lifecycle.capture();
|
|
1118
|
+
resetPromise = void 0;
|
|
606
1119
|
if (refreshPromise) return refreshPromise;
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
if (
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
1120
|
+
const { rejectedToken } = refreshOptions;
|
|
1121
|
+
const hadToken = token !== void 0;
|
|
1122
|
+
const baseline = token?.generation;
|
|
1123
|
+
const pending = operation(authority, async () => {
|
|
1124
|
+
const key = await authenticatedKey(authority);
|
|
1125
|
+
authority.assertCurrent();
|
|
1126
|
+
if (channel) {
|
|
1127
|
+
const recognition = await currentRecognition(authority);
|
|
1128
|
+
authority.assertCurrent();
|
|
1129
|
+
const version = await readCookieSessionVersion(scope.id);
|
|
1130
|
+
authority.assertCurrent();
|
|
1131
|
+
if (!version?.active || version.jkt !== key.jkt) throw loginRequired();
|
|
1132
|
+
const reusable = () => {
|
|
1133
|
+
const current2 = token;
|
|
1134
|
+
if (current2?.jkt === key.jkt && current2.accessToken !== rejectedToken && current2.expiresAt > now() && current2.generation === version.generation && tokenRecognition === recognition)
|
|
1135
|
+
return current2;
|
|
1136
|
+
};
|
|
1137
|
+
const current = reusable();
|
|
1138
|
+
if (current && (rejectedToken || !hadToken || current.generation !== baseline)) return current;
|
|
1139
|
+
if (!hadToken || version.generation !== baseline) {
|
|
1140
|
+
const peer = await requestPeer(authority, key.jkt, version.generation, recognition, rejectedToken);
|
|
1141
|
+
authority.assertCurrent();
|
|
1142
|
+
if (peer) return peer;
|
|
1143
|
+
const winner = reusable();
|
|
1144
|
+
if (winner) return winner;
|
|
625
1145
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
)
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
1146
|
+
}
|
|
1147
|
+
if (!channel && rejectedToken && token && token.accessToken !== rejectedToken && token.expiresAt > now() && tokenRecognition === await currentRecognition(authority)) {
|
|
1148
|
+
authority.assertCurrent();
|
|
1149
|
+
return token;
|
|
1150
|
+
}
|
|
1151
|
+
authority.assertCurrent();
|
|
1152
|
+
const handle = sessionTransport === "body" ? readStorage(storageKey) : void 0;
|
|
1153
|
+
if (sessionTransport === "body" && (!handle?.sessionId || handle.jkt !== key.jkt)) throw loginRequired();
|
|
1154
|
+
return acceptResponse(
|
|
1155
|
+
await post(authority, "refresh", handle ? { sessionId: handle.sessionId } : {}, key),
|
|
1156
|
+
key,
|
|
1157
|
+
authority
|
|
1158
|
+
);
|
|
1159
|
+
});
|
|
639
1160
|
refreshPromise = pending;
|
|
640
1161
|
void pending.finally(() => {
|
|
641
1162
|
if (refreshPromise === pending) refreshPromise = void 0;
|
|
@@ -644,34 +1165,198 @@ var createOidcVaultDpopSession = (options) => {
|
|
|
644
1165
|
return pending;
|
|
645
1166
|
},
|
|
646
1167
|
async logout() {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
1168
|
+
const authority = lifecycle.capture();
|
|
1169
|
+
refreshPromise = void 0;
|
|
1170
|
+
resetPromise = void 0;
|
|
1171
|
+
await operation(authority, async () => {
|
|
1172
|
+
const key = await authenticatedKey(authority);
|
|
1173
|
+
authority.assertCurrent();
|
|
650
1174
|
const handle = sessionTransport === "body" ? readStorage(storageKey) : void 0;
|
|
651
1175
|
if (sessionTransport === "body" && !handle?.sessionId) throw loginRequired();
|
|
652
|
-
const { value } = await post("logout", handle ? { sessionId: handle.sessionId } : {}, key);
|
|
1176
|
+
const { value } = await post(authority, "logout", handle ? { sessionId: handle.sessionId } : {}, key);
|
|
1177
|
+
authority.assertCurrent();
|
|
653
1178
|
if (typeof value !== "object" || value === null || !("loggedOut" in value) || value.loggedOut !== true) {
|
|
654
1179
|
throw new OidcVaultDpopClientError("INVALID_LOGOUT_RESPONSE", "The logout response is invalid.");
|
|
655
1180
|
}
|
|
656
|
-
|
|
1181
|
+
clearLocal();
|
|
1182
|
+
await invalidateCookie(authority, key.jkt);
|
|
657
1183
|
});
|
|
658
1184
|
},
|
|
659
1185
|
getAccessToken: () => token,
|
|
660
|
-
getKey
|
|
661
|
-
|
|
1186
|
+
async getKey() {
|
|
1187
|
+
const authority = stateAuthority();
|
|
1188
|
+
try {
|
|
1189
|
+
const pending = !token ? readStorage(pendingKey) : void 0;
|
|
1190
|
+
if (pending) {
|
|
1191
|
+
await existingKey(authority, pending.jkt);
|
|
1192
|
+
authority.assertCurrent();
|
|
1193
|
+
throw loginRequired();
|
|
1194
|
+
}
|
|
1195
|
+
return await authenticatedKey(authority);
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
authority.assertCurrent();
|
|
1198
|
+
if (error instanceof OidcVaultDpopClientError && error.code === "LOGIN_REQUIRED") throw error;
|
|
1199
|
+
return operation(authority, async () => {
|
|
1200
|
+
throw error;
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
},
|
|
1204
|
+
async clear() {
|
|
1205
|
+
const oldJkt = token?.jkt;
|
|
1206
|
+
const authority = invalidateLocal();
|
|
1207
|
+
if (!channel) return;
|
|
1208
|
+
await lifecycle.enqueue(authority, () => withCookieLock(() => invalidateCookie(authority, oldJkt)));
|
|
1209
|
+
},
|
|
1210
|
+
async resetKey() {
|
|
1211
|
+
if (resetPromise) return resetPromise;
|
|
1212
|
+
const oldJkt = token?.jkt;
|
|
1213
|
+
const authority = invalidateLocal();
|
|
1214
|
+
const pending = lifecycle.enqueue(
|
|
1215
|
+
authority,
|
|
1216
|
+
() => withCookieLock(async () => {
|
|
1217
|
+
authority.assertCurrent();
|
|
1218
|
+
const previous = await removeDpopScope(scope.id, authority);
|
|
1219
|
+
authority.assertCurrent();
|
|
1220
|
+
if (previous.keyPresent || previous.cookiePresent)
|
|
1221
|
+
channel?.postMessage({
|
|
1222
|
+
type: "reset-key",
|
|
1223
|
+
sender,
|
|
1224
|
+
jkt: previous.version?.jkt ?? oldJkt ?? "",
|
|
1225
|
+
generation: previous.version?.generation ?? "",
|
|
1226
|
+
keyJkt: previous.keyJkt
|
|
1227
|
+
});
|
|
1228
|
+
})
|
|
1229
|
+
);
|
|
1230
|
+
resetPromise = pending;
|
|
1231
|
+
void pending.finally(() => {
|
|
1232
|
+
if (resetPromise === pending) resetPromise = void 0;
|
|
1233
|
+
}).catch(() => {
|
|
1234
|
+
});
|
|
1235
|
+
await pending;
|
|
1236
|
+
},
|
|
662
1237
|
dispose() {
|
|
663
|
-
|
|
664
|
-
|
|
1238
|
+
lifecycle.dispose();
|
|
1239
|
+
publication++;
|
|
1240
|
+
refreshPromise = void 0;
|
|
1241
|
+
resetPromise = void 0;
|
|
665
1242
|
token = void 0;
|
|
666
1243
|
tokenRecognition = void 0;
|
|
1244
|
+
nonces.clear();
|
|
1245
|
+
for (const waiter of peerWaiters) waiter.finish();
|
|
1246
|
+
if (channel) channel.onmessage = null;
|
|
667
1247
|
channel?.close();
|
|
668
|
-
peerWaiters.clear();
|
|
669
1248
|
}
|
|
670
1249
|
};
|
|
671
1250
|
return Object.freeze(session);
|
|
672
1251
|
};
|
|
673
1252
|
|
|
1253
|
+
// src/auth-challenge.ts
|
|
1254
|
+
var TOKEN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
1255
|
+
var TOKEN68 = /^[A-Za-z0-9._~+/-]+=*$/;
|
|
1256
|
+
var PARAMETER = /^([!#$%&'*+.^_`|~0-9A-Za-z-]+)[ \t]*=[ \t]*(.*)$/;
|
|
1257
|
+
var CHALLENGE = /^([!#$%&'*+.^_`|~0-9A-Za-z-]+)(?: +(.+))?$/;
|
|
1258
|
+
var partsOf = (header) => {
|
|
1259
|
+
const parts = [];
|
|
1260
|
+
let start = 0;
|
|
1261
|
+
const addPart = (end) => {
|
|
1262
|
+
while (start < end && (header[start] === " " || header[start] === " ")) start++;
|
|
1263
|
+
while (end > start && (header[end - 1] === " " || header[end - 1] === " ")) end--;
|
|
1264
|
+
parts.push(header.slice(start, end));
|
|
1265
|
+
};
|
|
1266
|
+
let quoted = false;
|
|
1267
|
+
let escaped = false;
|
|
1268
|
+
for (let index = 0; index < header.length; index++) {
|
|
1269
|
+
const character = header[index];
|
|
1270
|
+
if (escaped) escaped = false;
|
|
1271
|
+
else if (quoted && character === "\\") escaped = true;
|
|
1272
|
+
else if (character === '"') quoted = !quoted;
|
|
1273
|
+
else if (!quoted && character === ",") {
|
|
1274
|
+
addPart(index);
|
|
1275
|
+
start = index + 1;
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (quoted || escaped) return void 0;
|
|
1279
|
+
addPart(header.length);
|
|
1280
|
+
return parts;
|
|
1281
|
+
};
|
|
1282
|
+
var parameterOf = (part) => {
|
|
1283
|
+
const match = PARAMETER.exec(part);
|
|
1284
|
+
if (!match) return void 0;
|
|
1285
|
+
const name = match[1].toLowerCase();
|
|
1286
|
+
const raw = match[2];
|
|
1287
|
+
if (TOKEN.test(raw)) return [name, raw];
|
|
1288
|
+
if (raw[0] !== '"') return void 0;
|
|
1289
|
+
let value = "";
|
|
1290
|
+
for (let index = 1; index < raw.length; index++) {
|
|
1291
|
+
const character = raw[index];
|
|
1292
|
+
if (character === '"') return index === raw.length - 1 ? [name, value] : void 0;
|
|
1293
|
+
if (character === "\\") {
|
|
1294
|
+
if (++index === raw.length) return void 0;
|
|
1295
|
+
value += raw[index];
|
|
1296
|
+
} else value += character;
|
|
1297
|
+
}
|
|
1298
|
+
return void 0;
|
|
1299
|
+
};
|
|
1300
|
+
var readDpopChallengeError = (header) => {
|
|
1301
|
+
if (typeof header !== "string" || header.length > 8192 || !/^[\t\x20-\x7e\x80-\xff]*$/.test(header)) return void 0;
|
|
1302
|
+
const parts = partsOf(header);
|
|
1303
|
+
if (!parts) return void 0;
|
|
1304
|
+
let parameters;
|
|
1305
|
+
let dpopParameters;
|
|
1306
|
+
let seenDpop = false;
|
|
1307
|
+
for (const part of parts) {
|
|
1308
|
+
const parameter = parameterOf(part);
|
|
1309
|
+
if (parameter) {
|
|
1310
|
+
if (!parameters || parameters.has(parameter[0])) return void 0;
|
|
1311
|
+
parameters.set(...parameter);
|
|
1312
|
+
continue;
|
|
1313
|
+
}
|
|
1314
|
+
const challenge = CHALLENGE.exec(part);
|
|
1315
|
+
if (!challenge) return void 0;
|
|
1316
|
+
const isDpop = challenge[1].toLowerCase() === "dpop";
|
|
1317
|
+
if (isDpop && seenDpop) return void 0;
|
|
1318
|
+
if (isDpop) seenDpop = true;
|
|
1319
|
+
parameters = void 0;
|
|
1320
|
+
const credentials = challenge[2];
|
|
1321
|
+
if (credentials !== void 0) {
|
|
1322
|
+
const first = parameterOf(credentials);
|
|
1323
|
+
if (first) parameters = new Map([first]);
|
|
1324
|
+
else if (!TOKEN68.test(credentials)) return void 0;
|
|
1325
|
+
}
|
|
1326
|
+
if (isDpop) dpopParameters = parameters;
|
|
1327
|
+
}
|
|
1328
|
+
const error = dpopParameters?.get("error");
|
|
1329
|
+
return error === "use_dpop_nonce" || error === "invalid_token" ? error : void 0;
|
|
1330
|
+
};
|
|
1331
|
+
|
|
674
1332
|
// src/auth-fetch.ts
|
|
1333
|
+
var invalidApiConfig = () => new OidcVaultDpopClientError("INVALID_DPOP_API_CONFIG", "The API auth scope configuration is invalid or ambiguous.");
|
|
1334
|
+
var selectApi = (apis, origin, pathname) => {
|
|
1335
|
+
if (!Array.isArray(apis)) throw invalidApiConfig();
|
|
1336
|
+
const scopes = /* @__PURE__ */ new Set();
|
|
1337
|
+
let selected;
|
|
1338
|
+
for (const candidate of apis) {
|
|
1339
|
+
if (!candidate || typeof candidate !== "object") throw invalidApiConfig();
|
|
1340
|
+
const { origin: configuredOrigin, pathPrefix: configuredPath, replayNamespace, credentials = "omit" } = candidate;
|
|
1341
|
+
if (typeof configuredOrigin !== "string" || typeof replayNamespace !== "string" || !/^[\x20-\x7e]{1,128}$/.test(replayNamespace) || credentials !== "omit" && credentials !== "include")
|
|
1342
|
+
throw invalidApiConfig();
|
|
1343
|
+
const normalizedOrigin = normalizeStaticOrigin(configuredOrigin);
|
|
1344
|
+
const pathPrefix = normalizeDpopPathPrefix(normalizedOrigin, configuredPath);
|
|
1345
|
+
const scope = JSON.stringify([normalizedOrigin, pathPrefix]);
|
|
1346
|
+
if (scopes.has(scope)) throw invalidApiConfig();
|
|
1347
|
+
scopes.add(scope);
|
|
1348
|
+
if (normalizedOrigin !== origin || pathPrefix !== "/" && pathname !== pathPrefix && !pathname.startsWith(`${pathPrefix}/`)) {
|
|
1349
|
+
continue;
|
|
1350
|
+
}
|
|
1351
|
+
if (!selected || pathPrefix.length > selected.pathPrefix.length) {
|
|
1352
|
+
selected = { origin: normalizedOrigin, pathPrefix, replayNamespace, credentials };
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
if (!selected) {
|
|
1356
|
+
throw new OidcVaultDpopClientError("DPOP_API_OUT_OF_SCOPE", "The API target is outside the configured auth scope.");
|
|
1357
|
+
}
|
|
1358
|
+
return selected;
|
|
1359
|
+
};
|
|
675
1360
|
var defaultNonces = /* @__PURE__ */ new WeakMap();
|
|
676
1361
|
var nonceCache = (context) => {
|
|
677
1362
|
if (context.nonces) return context.nonces;
|
|
@@ -710,14 +1395,24 @@ var replayableBody = (body) => {
|
|
|
710
1395
|
}
|
|
711
1396
|
throw new OidcVaultDpopClientError("DPOP_BODY_NOT_REPLAYABLE", "Retry requires a replayable request body.");
|
|
712
1397
|
};
|
|
713
|
-
var
|
|
1398
|
+
var assertLiveAccessToken = (expiresAt, now) => {
|
|
1399
|
+
if (!Number.isSafeInteger(now) || now < 0)
|
|
1400
|
+
throw new OidcVaultDpopClientError("INVALID_DPOP_TIME", "The proof clock is invalid.");
|
|
1401
|
+
if (!Number.isSafeInteger(expiresAt) || expiresAt <= now)
|
|
1402
|
+
throw new OidcVaultDpopClientError(
|
|
1403
|
+
"DPOP_ACCESS_TOKEN_EXPIRED",
|
|
1404
|
+
"The DPoP access token has expired. Refresh before retrying."
|
|
1405
|
+
);
|
|
1406
|
+
};
|
|
714
1407
|
var fetchWithDpop = async (context, input, options = {}) => {
|
|
715
|
-
const
|
|
716
|
-
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1408
|
+
const { signal } = options;
|
|
1409
|
+
assertNotAborted(signal);
|
|
1410
|
+
const inputUrl = typeof input === "string" ? input : input.href;
|
|
1411
|
+
const target = normalizeDpopTarget(inputUrl);
|
|
1412
|
+
const url = new URL(inputUrl);
|
|
1413
|
+
url.pathname = new URL(target).pathname;
|
|
1414
|
+
const { href, origin, pathname } = url;
|
|
1415
|
+
const api = selectApi(context.apis, origin, pathname);
|
|
721
1416
|
const { session, fetch: request = globalThis.fetch.bind(globalThis), now = Date.now } = context;
|
|
722
1417
|
const { retry, ...init } = options;
|
|
723
1418
|
const method = (init.method ?? "GET").toUpperCase();
|
|
@@ -730,58 +1425,80 @@ var fetchWithDpop = async (context, input, options = {}) => {
|
|
|
730
1425
|
if (headers.has(name))
|
|
731
1426
|
throw new OidcVaultDpopClientError("DPOP_AUTH_HEADER_OVERRIDE", "The DPoP helper owns authentication headers.");
|
|
732
1427
|
}
|
|
733
|
-
const space = JSON.stringify(["api",
|
|
1428
|
+
const space = JSON.stringify(["api", api.origin, api.replayNamespace]);
|
|
734
1429
|
const nonces = nonceCache(context);
|
|
735
1430
|
let nonceRetried = false;
|
|
736
1431
|
let refreshed = false;
|
|
737
1432
|
let credential = session.getAccessToken();
|
|
738
1433
|
if (!credential || credential.expiresAt <= now()) {
|
|
739
|
-
credential = await session.refresh();
|
|
1434
|
+
credential = await abortable(signal, () => session.refresh());
|
|
740
1435
|
refreshed = true;
|
|
741
1436
|
}
|
|
742
1437
|
for (; ; ) {
|
|
743
|
-
|
|
1438
|
+
assertNotAborted(signal);
|
|
744
1439
|
if (credential.tokenType !== "DPoP") {
|
|
745
|
-
await session.clear();
|
|
1440
|
+
await abortable(signal, () => session.clear());
|
|
1441
|
+
assertNotAborted(signal);
|
|
746
1442
|
throw new OidcVaultDpopClientError(
|
|
747
1443
|
"DPOP_TOKEN_TYPE_REQUIRED",
|
|
748
1444
|
"A DPoP access token is required. Sign in again.",
|
|
749
1445
|
true
|
|
750
1446
|
);
|
|
751
1447
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1448
|
+
assertLiveAccessToken(credential.expiresAt, now());
|
|
1449
|
+
const accessToken = credential.accessToken;
|
|
1450
|
+
const key = await abortable(signal, () => session.getKey());
|
|
1451
|
+
assertNotAborted(signal);
|
|
1452
|
+
const { jkt } = key;
|
|
1453
|
+
if (jkt !== credential.jkt) {
|
|
1454
|
+
await abortable(signal, () => session.clear());
|
|
1455
|
+
assertNotAborted(signal);
|
|
755
1456
|
throw loginRequired();
|
|
756
1457
|
}
|
|
757
|
-
const proof = await
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
1458
|
+
const proof = await abortable(
|
|
1459
|
+
signal,
|
|
1460
|
+
() => createDpopProof(key, {
|
|
1461
|
+
method,
|
|
1462
|
+
url: target,
|
|
1463
|
+
accessToken,
|
|
1464
|
+
nonce: nonces.get(space, jkt),
|
|
1465
|
+
now,
|
|
1466
|
+
signal: signal ?? void 0
|
|
1467
|
+
})
|
|
1468
|
+
);
|
|
1469
|
+
assertNotAborted(signal);
|
|
1470
|
+
assertLiveAccessToken(credential.expiresAt, now());
|
|
764
1471
|
const attemptHeaders = new Headers(headers);
|
|
765
|
-
attemptHeaders.set("Authorization", `DPoP ${
|
|
1472
|
+
attemptHeaders.set("Authorization", `DPoP ${accessToken}`);
|
|
766
1473
|
attemptHeaders.set("DPoP", proof);
|
|
767
|
-
const response = await
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1474
|
+
const response = await abortable(
|
|
1475
|
+
signal,
|
|
1476
|
+
() => request(href, {
|
|
1477
|
+
...init,
|
|
1478
|
+
method,
|
|
1479
|
+
body: body(),
|
|
1480
|
+
headers: attemptHeaders,
|
|
1481
|
+
credentials: api.credentials,
|
|
1482
|
+
redirect: "error",
|
|
1483
|
+
cache: "no-store"
|
|
1484
|
+
}),
|
|
1485
|
+
cancelResponse
|
|
1486
|
+
);
|
|
1487
|
+
if (signal?.aborted) {
|
|
1488
|
+
cancelResponse(response);
|
|
1489
|
+
assertNotAborted(signal);
|
|
1490
|
+
}
|
|
1491
|
+
const challenge = response.status === 401 ? readDpopChallengeError(response.headers.get("WWW-Authenticate")) : void 0;
|
|
1492
|
+
const learnedNonce = nonces.remember(space, jkt, response.headers.get("DPoP-Nonce"));
|
|
1493
|
+
if (challenge === "use_dpop_nonce" && canRetry && !nonceRetried && learnedNonce) {
|
|
777
1494
|
nonceRetried = true;
|
|
778
|
-
|
|
1495
|
+
cancelResponse(response);
|
|
779
1496
|
continue;
|
|
780
1497
|
}
|
|
781
|
-
if (
|
|
1498
|
+
if (challenge === "invalid_token" && canRetry && !refreshed) {
|
|
782
1499
|
refreshed = true;
|
|
783
|
-
|
|
784
|
-
credential = await session.refresh({ rejectedToken:
|
|
1500
|
+
cancelResponse(response);
|
|
1501
|
+
credential = await abortable(signal, () => session.refresh({ rejectedToken: accessToken }));
|
|
785
1502
|
continue;
|
|
786
1503
|
}
|
|
787
1504
|
return response;
|