@web-ts-toolkit/express-oidc-vault 0.49.0 → 0.51.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 +46 -5
- package/index.d.mts +30 -1
- package/index.d.ts +30 -1
- package/index.js +64 -17
- package/index.mjs +64 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -318,6 +318,47 @@ The shared nonce policy signs versioned, random HMAC-SHA-256 challenges bound to
|
|
|
318
318
|
|
|
319
319
|
Sessions, authorization transactions, and exchange codes carry optional `deviceBinding: { type: 'dpop', jkt: string }`; transactions/codes also carry optional `browserBindingHash`. Only the canonical RFC 7638 SHA-256 thumbprint is persisted, without a JWK, private key, proof algorithm, or historical mode. Core retains security-owned binding independently of mutable precreate hooks and issuer inputs. All three built-ins implement live preflight reads, atomic exact/null match-and-consume, lineage revocation context, immutable rotation, and shared bounded replay admission; see the provider contracts below. These store primitives do not themselves verify HTTP proofs.
|
|
320
320
|
|
|
321
|
+
#### Per-check DPoP failure overrides
|
|
322
|
+
|
|
323
|
+
Steps 6–8 have independent log-and-continue settings. Each check still runs; an enabled setting writes every failure to the server's `console.warn` and continues to the next check. Both vault POSTs and API middleware read these defaults from `process.env` when constructed:
|
|
324
|
+
|
|
325
|
+
| Step | Check | Environment variable | `deviceBinding` option |
|
|
326
|
+
| ---- | ----------------------------------------------------------------------------------- | ------------------------------------------ | ------------------------ |
|
|
327
|
+
| 6 | HTTP method and normalized pinned URL (`htm`/`htu`), including target normalization | `OIDC_VAULT_DPOP_IGNORE_TARGET_FAILURE` | `ignoreTargetFailure` |
|
|
328
|
+
| 7 | Signed proof freshness (`iat` age and clock skew) | `OIDC_VAULT_DPOP_IGNORE_FRESHNESS_FAILURE` | `ignoreFreshnessFailure` |
|
|
329
|
+
| 8 | Atomic JTI replay admission, including duplicates, capacity, and provider failures | `OIDC_VAULT_DPOP_IGNORE_REPLAY_FAILURE` | `ignoreReplayFailure` |
|
|
330
|
+
|
|
331
|
+
Only the exact environment value `true` enables an override; unset, `false`, or any other value retains enforcement. Set whichever flags you need before constructing middleware, for example:
|
|
332
|
+
|
|
333
|
+
```dotenv
|
|
334
|
+
OIDC_VAULT_DPOP_IGNORE_TARGET_FAILURE=true
|
|
335
|
+
OIDC_VAULT_DPOP_IGNORE_FRESHNESS_FAILURE=false
|
|
336
|
+
OIDC_VAULT_DPOP_IGNORE_REPLAY_FAILURE=true
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Explicit booleans on `OidcVaultDeviceBindingOptions` / `OidcVaultApiDeviceBindingOptions` take precedence over the environment for that policy:
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
import type { OidcVaultDeviceBindingOptions } from '@web-ts-toolkit/express-oidc-vault';
|
|
343
|
+
|
|
344
|
+
const deviceBinding: OidcVaultDeviceBindingOptions = {
|
|
345
|
+
mode: 'required',
|
|
346
|
+
ignoreTargetFailure: true,
|
|
347
|
+
ignoreFreshnessFailure: false,
|
|
348
|
+
ignoreReplayFailure: false,
|
|
349
|
+
};
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
An ignored freshness failure still reaches nonce verification and replay admission. To give replay admission a live bounded expiry independently of the rejected `iat`, core uses `now + (proofMaxAgeSeconds + clockSkewSeconds) * 1000` (65 seconds with defaults). A failed configured proof clock is logged and falls back to `Date.now`. Successful replay reservations retain their expiry; duplicates never renew them, even when their failure is ignored. Signature, header/JWK, claim-shape, original-key, API `ath`, and nonce checks retain their normal enforcement.
|
|
353
|
+
|
|
354
|
+
Console output identifies the step, option/environment variable, and original error (including replay provider causes), for example:
|
|
355
|
+
|
|
356
|
+
```text
|
|
357
|
+
[express-oidc-vault] DPoP step 8 (replay) failed; continuing (ignoreReplayFailure=true; OIDC_VAULT_DPOP_IGNORE_REPLAY_FAILURE).
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Environment changes take effect on newly constructed middleware. Ignored failures go directly to the console; `onError` handles failures that stop the request.
|
|
361
|
+
|
|
321
362
|
### POST login and browser-authenticated callback
|
|
322
363
|
|
|
323
364
|
Configure the vault with the existing stronger store and an opt-in policy:
|
|
@@ -1443,9 +1484,9 @@ DPoP replay is a reservation per accepted proof, including API traffic; it is su
|
|
|
1443
1484
|
|
|
1444
1485
|
`expiresAt` is a future safe-integer epoch in milliseconds, with at most **360000 ms** remaining. Invalid, fractional, unsafe, nonfinite, expired (`<= now`), or overlong windows return `false` without allocation. Valid expired keys may be reserved again. All instances must use identical namespaces/proof windows and synchronized clocks. The approved proof defaults retain at most 70 seconds of validity; provision capacity for unique proofs/second × retained window plus headroom.
|
|
1445
1486
|
|
|
1446
|
-
Core derives `replayKey = 'dpop:v1:' + base64url(SHA-256(JSON([effectiveNamespace, jkt, jti])))` and expiry **`(iat + proofMaxAgeSeconds + clockSkewSeconds) * 1000`**. Vault space is `['vault', normalizedBackendOrigin, normalizedBasePath, exactIssuer, exactClientId]`; API space is `['api', normalizedPublicOrigin, replayNamespace]`. API path prefix is a target correction, not replay partitioning. The 360000ms ceiling includes future iat skew (maximum age 300 + twice skew 30); no duplicate extends its reservation.
|
|
1487
|
+
Core derives `replayKey = 'dpop:v1:' + base64url(SHA-256(JSON([effectiveNamespace, jkt, jti])))` and expiry **`(iat + proofMaxAgeSeconds + clockSkewSeconds) * 1000`**. An explicitly ignored freshness failure instead uses the bounded current-time expiry described under [per-check overrides](#per-check-dpop-failure-overrides). Vault space is `['vault', normalizedBackendOrigin, normalizedBasePath, exactIssuer, exactClientId]`; API space is `['api', normalizedPublicOrigin, replayNamespace]`. API path prefix is a target correction, not replay partitioning. The 360000ms ceiling includes future iat skew (maximum age 300 + twice skew 30); no duplicate extends its reservation.
|
|
1447
1488
|
|
|
1448
|
-
Every provider accepts `dpopReplayMaxEntries?: number`, default **100000**, positive safe integer. Capacity is per memory object, Redis prefix, or paired Mongo replay collections, shared across backend clients. Configure the same limit on every shared client. Duplicates return `false` even when full; new reservations throw root-exported `OidcVaultDpopReplayCapacityError` at capacity. Live reservations are never evicted
|
|
1489
|
+
Every provider accepts `dpopReplayMaxEntries?: number`, default **100000**, positive safe integer. Capacity is per memory object, Redis prefix, or paired Mongo replay collections, shared across backend clients. Configure the same limit on every shared client. Duplicates return `false` even when full; new reservations throw root-exported `OidcVaultDpopReplayCapacityError` at capacity. Live reservations are never evicted. By default, API middleware and all DPoP-bearing vault POSTs map failures/capacity to sanitized `503 OIDC_VAULT_DPOP_REPLAY_UNAVAILABLE`; `ignoreReplayFailure` logs these failures and continues after attempting admission.
|
|
1449
1490
|
|
|
1450
1491
|
| Provider | Admission/expiry work |
|
|
1451
1492
|
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -1891,7 +1932,7 @@ Authorization: DPoP <local-access-token>
|
|
|
1891
1932
|
DPoP: <fresh-signed-proof-jwt>
|
|
1892
1933
|
```
|
|
1893
1934
|
|
|
1894
|
-
The proof has protected `{ typ: 'dpop+jwt', alg: 'ES256', jwk: <public-P-256-JWK> }` and signed `htm`, absolute `htu` without query/fragment, integer `iat`, fresh `jti`, and API `ath = base64url(SHA-256(ASCII(accessToken)))`. A configured nonce also supplies the signed `nonce`. Signature verification precedes payload/target/key/hash checks.
|
|
1935
|
+
The proof has protected `{ typ: 'dpop+jwt', alg: 'ES256', jwk: <public-P-256-JWK> }` and signed `htm`, absolute `htu` without query/fragment, integer `iat`, fresh `jti`, and API `ath = base64url(SHA-256(ASCII(accessToken)))`. A configured nonce also supplies the signed `nonce`. Signature verification precedes payload/target/key/hash checks. By default, private/symmetric/remote-key JWKs, critical headers, wrong key/signature/method/URL/hash, and duplicate/stale/replayed proofs fail. The independent [per-check overrides](#per-check-dpop-failure-overrides) apply to method/URL, freshness, and replay failures. Bounds: compact proof 8192 bytes, decoded protected header/JWK 2048 bytes, printable ASCII JTI 1–128 bytes, nonce at most 512 bytes. Raw duplicate Authorization/DPoP fields and comma-joined credentials/proofs are rejected. Generate at least 128 random bits for every JTI and a new proof for every request/retry.
|
|
1895
1936
|
|
|
1896
1937
|
For optional unbound Bearer requests, an omitted proof stays on the unbound path. If a proof is supplied, it must still pass signature/target/API `ath`/nonce/shared replay and never creates `auth.deviceBinding`.
|
|
1897
1938
|
|
|
@@ -1899,7 +1940,7 @@ API replay uses the shared `['api', normalizedPublicOrigin, replayNamespace]` pr
|
|
|
1899
1940
|
|
|
1900
1941
|
#### API errors and nonce retry
|
|
1901
1942
|
|
|
1902
|
-
|
|
1943
|
+
With default failure enforcement and the ES256 policy (`algs` lists the configured allowlist in order):
|
|
1903
1944
|
|
|
1904
1945
|
| Condition | HTTP / code | `WWW-Authenticate` |
|
|
1905
1946
|
| ---------------------------------------------------------- | ---------------------------------------------------------------------------- | ----------------------------------------------- |
|
|
@@ -1933,7 +1974,7 @@ Vault routes and API middleware return sanitized `{ code, message }`. Invalid pr
|
|
|
1933
1974
|
| 400 | `OIDC_VAULT_INVALID_FINGERPRINT` | `Fingerprint signal is invalid.` |
|
|
1934
1975
|
| 403 | `OIDC_VAULT_FINGERPRINT_REAUTH_REQUIRED` | `Browser recognition changed; sign in again.` |
|
|
1935
1976
|
|
|
1936
|
-
Source failures retain `403 OIDC_VAULT_UNTRUSTED_ORIGIN` with `Login request origin is not trusted.` / `Exchange request origin is not trusted.` / `Refresh request origin is not trusted.` / `Logout request origin is not trusted.`. An authenticated provider-error callback returns fixed `400 OIDC_VAULT_CALLBACK_ERROR` / `OIDC callback failed.`.
|
|
1977
|
+
Source failures retain `403 OIDC_VAULT_UNTRUSTED_ORIGIN` with `Login request origin is not trusted.` / `Exchange request origin is not trusted.` / `Refresh request origin is not trusted.` / `Logout request origin is not trusted.`. An authenticated provider-error callback returns fixed `400 OIDC_VAULT_CALLBACK_ERROR` / `OIDC callback failed.`. With default enforcement, replay storage failure/capacity is a fail-closed 503 with no auth challenge; invalid/missing proof 401s use `invalid_dpop_proof`, and API nonce 401s use `use_dpop_nonce`. Vault nonce 400s carry the one `DPoP-Nonce` header without `WWW-Authenticate`. Every response retains `no-store`; failures that stop the request reach `hooks.onError` or the separate API `onError`, with replay provider diagnostics in `error.cause`. Independently ignored failures are logged to `console.warn` as described under [per-check overrides](#per-check-dpop-failure-overrides).
|
|
1937
1978
|
|
|
1938
1979
|
Recommended separation:
|
|
1939
1980
|
|
package/index.d.mts
CHANGED
|
@@ -41,6 +41,8 @@ interface OidcVaultDpopNonceOptions {
|
|
|
41
41
|
* Signature verification precedes claims; strict iat validity is
|
|
42
42
|
* nowSeconds - age - skew < iat <= nowSeconds + skew. APIs additionally match
|
|
43
43
|
* original verified cnf.jkt and SHA-256 ath of the ASCII access token.
|
|
44
|
+
* Independent failure overrides below run their checks and log to console.warn
|
|
45
|
+
* before continuing. Environment defaults are captured at middleware construction.
|
|
44
46
|
*/
|
|
45
47
|
interface OidcVaultDpopProofOptions {
|
|
46
48
|
/** Nonempty asymmetric allowlist; default `['ES256']` (P-256). RSA proofs require 2048–4096 bits. */
|
|
@@ -51,6 +53,24 @@ interface OidcVaultDpopProofOptions {
|
|
|
51
53
|
clockSkewSeconds?: number;
|
|
52
54
|
/** Off by default. Challenges are at most 512 bytes; retry once with a fresh proof, never reuse its JTI. */
|
|
53
55
|
nonce?: false | OidcVaultDpopNonceOptions;
|
|
56
|
+
/**
|
|
57
|
+
* Step 6: log failed method/normalized URL checks and continue.
|
|
58
|
+
* Defaults to OIDC_VAULT_DPOP_IGNORE_TARGET_FAILURE === 'true'; otherwise false.
|
|
59
|
+
* An explicit boolean overrides the environment for this vault/API policy.
|
|
60
|
+
*/
|
|
61
|
+
ignoreTargetFailure?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Step 7: log failed proof age/skew checks and continue, retaining replay tracking
|
|
64
|
+
* with a bounded expiry from the current time. A failed clock uses Date.now.
|
|
65
|
+
* Defaults to OIDC_VAULT_DPOP_IGNORE_FRESHNESS_FAILURE === 'true'; otherwise false.
|
|
66
|
+
*/
|
|
67
|
+
ignoreFreshnessFailure?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Step 8: attempt atomic replay reservation, log duplicate/admission/provider
|
|
70
|
+
* failures, and continue. Successful reservations retain their normal lifetime.
|
|
71
|
+
* Defaults to OIDC_VAULT_DPOP_IGNORE_REPLAY_FAILURE === 'true'; otherwise false.
|
|
72
|
+
*/
|
|
73
|
+
ignoreReplayFailure?: boolean;
|
|
54
74
|
}
|
|
55
75
|
/**
|
|
56
76
|
* Opt-in sender constraint. `optional` permits legacy unbound records without
|
|
@@ -273,10 +293,11 @@ interface OidcVaultDpopReplayStore {
|
|
|
273
293
|
/**
|
|
274
294
|
* Reserve once through expiry. Duplicate/invalid/expired input returns false
|
|
275
295
|
* without extending expiry or allocating state. Capacity/provider failures
|
|
276
|
-
* throw; callers
|
|
296
|
+
* throw; callers fail closed by default, never evict live reservations. Built-ins
|
|
277
297
|
* default to 100000 entries per shared namespace. Expired entries awaiting
|
|
278
298
|
* bounded cleanup may conservatively occupy capacity. Duplicate detection
|
|
279
299
|
* precedes capacity rejection; capacity throws `OidcVaultDpopReplayCapacityError`.
|
|
300
|
+
* A proof policy with ignoreReplayFailure logs a failed admission and continues.
|
|
280
301
|
*/
|
|
281
302
|
reserveDpopProof(input: ReserveDpopProofInput): Promise<boolean>;
|
|
282
303
|
}
|
|
@@ -937,6 +958,10 @@ declare const OIDC_VAULT_ROUTE_PATHS: Record<OidcVaultRouteName, string>;
|
|
|
937
958
|
* callbacks/validator methods, URLs, algorithms and secret bytes; the replay
|
|
938
959
|
* store remains the shared atomic service. Responses carry no-store and fixed
|
|
939
960
|
* errors; onError privately observes originals and cannot override failures.
|
|
961
|
+
* The independent OIDC_VAULT_DPOP_IGNORE_TARGET_FAILURE,
|
|
962
|
+
* OIDC_VAULT_DPOP_IGNORE_FRESHNESS_FAILURE and OIDC_VAULT_DPOP_IGNORE_REPLAY_FAILURE
|
|
963
|
+
* environment defaults are captured at construction; ignored failures log to
|
|
964
|
+
* console.warn. Explicit deviceBinding booleans override those defaults.
|
|
940
965
|
*/
|
|
941
966
|
declare function createOidcVaultAccessTokenMiddleware(options: OidcVaultAccessTokenMiddlewareOptions): RequestHandler;
|
|
942
967
|
|
|
@@ -985,6 +1010,10 @@ declare function normalizeOidcVaultBasePath(value?: string): string;
|
|
|
985
1010
|
* an enabled nonce secret is copied into private owned storage. Separate opt-in
|
|
986
1011
|
* `fingerprintRecognition` settings are detached/frozen; recognition enrolls
|
|
987
1012
|
* only at POST login and checks exchange/refresh before credential mutation.
|
|
1013
|
+
* The independent OIDC_VAULT_DPOP_IGNORE_TARGET_FAILURE,
|
|
1014
|
+
* OIDC_VAULT_DPOP_IGNORE_FRESHNESS_FAILURE and OIDC_VAULT_DPOP_IGNORE_REPLAY_FAILURE
|
|
1015
|
+
* environment defaults are captured with the proof policy. Ignored failures
|
|
1016
|
+
* log to console.warn; explicit deviceBinding booleans override those defaults.
|
|
988
1017
|
*/
|
|
989
1018
|
declare function createOidcVaultMiddleware(options: OidcVaultOptions): Router;
|
|
990
1019
|
|
package/index.d.ts
CHANGED
|
@@ -41,6 +41,8 @@ interface OidcVaultDpopNonceOptions {
|
|
|
41
41
|
* Signature verification precedes claims; strict iat validity is
|
|
42
42
|
* nowSeconds - age - skew < iat <= nowSeconds + skew. APIs additionally match
|
|
43
43
|
* original verified cnf.jkt and SHA-256 ath of the ASCII access token.
|
|
44
|
+
* Independent failure overrides below run their checks and log to console.warn
|
|
45
|
+
* before continuing. Environment defaults are captured at middleware construction.
|
|
44
46
|
*/
|
|
45
47
|
interface OidcVaultDpopProofOptions {
|
|
46
48
|
/** Nonempty asymmetric allowlist; default `['ES256']` (P-256). RSA proofs require 2048–4096 bits. */
|
|
@@ -51,6 +53,24 @@ interface OidcVaultDpopProofOptions {
|
|
|
51
53
|
clockSkewSeconds?: number;
|
|
52
54
|
/** Off by default. Challenges are at most 512 bytes; retry once with a fresh proof, never reuse its JTI. */
|
|
53
55
|
nonce?: false | OidcVaultDpopNonceOptions;
|
|
56
|
+
/**
|
|
57
|
+
* Step 6: log failed method/normalized URL checks and continue.
|
|
58
|
+
* Defaults to OIDC_VAULT_DPOP_IGNORE_TARGET_FAILURE === 'true'; otherwise false.
|
|
59
|
+
* An explicit boolean overrides the environment for this vault/API policy.
|
|
60
|
+
*/
|
|
61
|
+
ignoreTargetFailure?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Step 7: log failed proof age/skew checks and continue, retaining replay tracking
|
|
64
|
+
* with a bounded expiry from the current time. A failed clock uses Date.now.
|
|
65
|
+
* Defaults to OIDC_VAULT_DPOP_IGNORE_FRESHNESS_FAILURE === 'true'; otherwise false.
|
|
66
|
+
*/
|
|
67
|
+
ignoreFreshnessFailure?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Step 8: attempt atomic replay reservation, log duplicate/admission/provider
|
|
70
|
+
* failures, and continue. Successful reservations retain their normal lifetime.
|
|
71
|
+
* Defaults to OIDC_VAULT_DPOP_IGNORE_REPLAY_FAILURE === 'true'; otherwise false.
|
|
72
|
+
*/
|
|
73
|
+
ignoreReplayFailure?: boolean;
|
|
54
74
|
}
|
|
55
75
|
/**
|
|
56
76
|
* Opt-in sender constraint. `optional` permits legacy unbound records without
|
|
@@ -273,10 +293,11 @@ interface OidcVaultDpopReplayStore {
|
|
|
273
293
|
/**
|
|
274
294
|
* Reserve once through expiry. Duplicate/invalid/expired input returns false
|
|
275
295
|
* without extending expiry or allocating state. Capacity/provider failures
|
|
276
|
-
* throw; callers
|
|
296
|
+
* throw; callers fail closed by default, never evict live reservations. Built-ins
|
|
277
297
|
* default to 100000 entries per shared namespace. Expired entries awaiting
|
|
278
298
|
* bounded cleanup may conservatively occupy capacity. Duplicate detection
|
|
279
299
|
* precedes capacity rejection; capacity throws `OidcVaultDpopReplayCapacityError`.
|
|
300
|
+
* A proof policy with ignoreReplayFailure logs a failed admission and continues.
|
|
280
301
|
*/
|
|
281
302
|
reserveDpopProof(input: ReserveDpopProofInput): Promise<boolean>;
|
|
282
303
|
}
|
|
@@ -937,6 +958,10 @@ declare const OIDC_VAULT_ROUTE_PATHS: Record<OidcVaultRouteName, string>;
|
|
|
937
958
|
* callbacks/validator methods, URLs, algorithms and secret bytes; the replay
|
|
938
959
|
* store remains the shared atomic service. Responses carry no-store and fixed
|
|
939
960
|
* errors; onError privately observes originals and cannot override failures.
|
|
961
|
+
* The independent OIDC_VAULT_DPOP_IGNORE_TARGET_FAILURE,
|
|
962
|
+
* OIDC_VAULT_DPOP_IGNORE_FRESHNESS_FAILURE and OIDC_VAULT_DPOP_IGNORE_REPLAY_FAILURE
|
|
963
|
+
* environment defaults are captured at construction; ignored failures log to
|
|
964
|
+
* console.warn. Explicit deviceBinding booleans override those defaults.
|
|
940
965
|
*/
|
|
941
966
|
declare function createOidcVaultAccessTokenMiddleware(options: OidcVaultAccessTokenMiddlewareOptions): RequestHandler;
|
|
942
967
|
|
|
@@ -985,6 +1010,10 @@ declare function normalizeOidcVaultBasePath(value?: string): string;
|
|
|
985
1010
|
* an enabled nonce secret is copied into private owned storage. Separate opt-in
|
|
986
1011
|
* `fingerprintRecognition` settings are detached/frozen; recognition enrolls
|
|
987
1012
|
* only at POST login and checks exchange/refresh before credential mutation.
|
|
1013
|
+
* The independent OIDC_VAULT_DPOP_IGNORE_TARGET_FAILURE,
|
|
1014
|
+
* OIDC_VAULT_DPOP_IGNORE_FRESHNESS_FAILURE and OIDC_VAULT_DPOP_IGNORE_REPLAY_FAILURE
|
|
1015
|
+
* environment defaults are captured with the proof policy. Ignored failures
|
|
1016
|
+
* log to console.warn; explicit deviceBinding booleans override those defaults.
|
|
988
1017
|
*/
|
|
989
1018
|
declare function createOidcVaultMiddleware(options: OidcVaultOptions): Router;
|
|
990
1019
|
|
package/index.js
CHANGED
|
@@ -290,6 +290,26 @@ var parseSelectedCookieValue = (headerValue, selectedName) => {
|
|
|
290
290
|
|
|
291
291
|
// src/device-binding-policy.ts
|
|
292
292
|
var import_node_net = require("net");
|
|
293
|
+
|
|
294
|
+
// src/dpop-failure-policy.ts
|
|
295
|
+
var CHECKS = {
|
|
296
|
+
target: { step: 6, option: "ignoreTargetFailure", env: "OIDC_VAULT_DPOP_IGNORE_TARGET_FAILURE" },
|
|
297
|
+
freshness: { step: 7, option: "ignoreFreshnessFailure", env: "OIDC_VAULT_DPOP_IGNORE_FRESHNESS_FAILURE" },
|
|
298
|
+
replay: { step: 8, option: "ignoreReplayFailure", env: "OIDC_VAULT_DPOP_IGNORE_REPLAY_FAILURE" }
|
|
299
|
+
};
|
|
300
|
+
var resolveDpopFailureOption = (value, check) => {
|
|
301
|
+
const { option, env } = CHECKS[check];
|
|
302
|
+
if (value === void 0) return process.env[env] === "true";
|
|
303
|
+
if (typeof value !== "boolean") throw new TypeError(`deviceBinding.${option} must be a boolean.`);
|
|
304
|
+
return value;
|
|
305
|
+
};
|
|
306
|
+
var handleDpopCheckFailure = (ignore, check, error) => {
|
|
307
|
+
if (!ignore) throw error;
|
|
308
|
+
const { step, option, env } = CHECKS[check];
|
|
309
|
+
console.warn(`[express-oidc-vault] DPoP step ${step} (${check}) failed; continuing (${option}=true; ${env}).`, error);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// src/device-binding-policy.ts
|
|
293
313
|
var isPlainRecord = (value) => {
|
|
294
314
|
if (typeof value !== "object" || value === null) return false;
|
|
295
315
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -306,7 +326,16 @@ var resolveInteger = (value, fallback, min, max, name) => {
|
|
|
306
326
|
var resolveDeviceBindingOptions = (value) => {
|
|
307
327
|
if (value === void 0) return void 0;
|
|
308
328
|
if (!isPlainRecord(value)) throw new Error("deviceBinding must be a plain options object.");
|
|
309
|
-
const {
|
|
329
|
+
const {
|
|
330
|
+
mode,
|
|
331
|
+
algorithms,
|
|
332
|
+
proofMaxAgeSeconds,
|
|
333
|
+
clockSkewSeconds,
|
|
334
|
+
nonce,
|
|
335
|
+
ignoreTargetFailure,
|
|
336
|
+
ignoreFreshnessFailure,
|
|
337
|
+
ignoreReplayFailure
|
|
338
|
+
} = value;
|
|
310
339
|
if (mode !== void 0 && mode !== "optional" && mode !== "required") {
|
|
311
340
|
throw new Error("deviceBinding.mode must be optional or required.");
|
|
312
341
|
}
|
|
@@ -338,7 +367,10 @@ var resolveDeviceBindingOptions = (value) => {
|
|
|
338
367
|
algorithms: Object.freeze([...new Set(allowlist)]),
|
|
339
368
|
proofMaxAgeSeconds: resolveInteger(proofMaxAgeSeconds, 60, 1, 300, "deviceBinding.proofMaxAgeSeconds"),
|
|
340
369
|
clockSkewSeconds: resolveInteger(clockSkewSeconds, 5, 0, 30, "deviceBinding.clockSkewSeconds"),
|
|
341
|
-
nonce: resolvedNonce
|
|
370
|
+
nonce: resolvedNonce,
|
|
371
|
+
ignoreTargetFailure: resolveDpopFailureOption(ignoreTargetFailure, "target"),
|
|
372
|
+
ignoreFreshnessFailure: resolveDpopFailureOption(ignoreFreshnessFailure, "freshness"),
|
|
373
|
+
ignoreReplayFailure: resolveDpopFailureOption(ignoreReplayFailure, "replay")
|
|
342
374
|
});
|
|
343
375
|
};
|
|
344
376
|
var DEVICE_BINDING_STORE_CAPABILITIES = [
|
|
@@ -669,6 +701,7 @@ var publicJwk = (value, alg) => {
|
|
|
669
701
|
var verifyDpopProof = async (input) => {
|
|
670
702
|
const { proof, method, targetUrl, proofOptions, expectedJkt, accessToken } = input;
|
|
671
703
|
const algorithms = [...proofOptions.algorithms];
|
|
704
|
+
const { ignoreTargetFailure } = proofOptions;
|
|
672
705
|
extractDpopProof({ count: 1, value: proof });
|
|
673
706
|
const [encodedHeader, encodedPayload, encodedSignature] = proof.split(".");
|
|
674
707
|
const header = parseObject(decodeBase64url2(encodedHeader, DPOP_PROTECTED_HEADER_MAX_BYTES));
|
|
@@ -694,8 +727,13 @@ var verifyDpopProof = async (input) => {
|
|
|
694
727
|
if (typeof iat !== "number" || !Number.isSafeInteger(iat) || iat < 0 || typeof jti !== "string" || jti.length < 1 || jti.length > 128 || /[^\x20-\x7e]/.test(jti)) {
|
|
695
728
|
throw invalidProof("DPoP signed iat/JTI must be nonnegative integer time and bounded printable ASCII.");
|
|
696
729
|
}
|
|
697
|
-
|
|
698
|
-
|
|
730
|
+
try {
|
|
731
|
+
const expectedTarget = typeof targetUrl === "function" ? targetUrl() : targetUrl;
|
|
732
|
+
if (htm !== method || typeof method !== "string" || method.length === 0 || normalizeDpopProofTarget(htu) !== expectedTarget) {
|
|
733
|
+
throw invalidProof("DPoP signed method or normalized public target does not match this request.");
|
|
734
|
+
}
|
|
735
|
+
} catch (error) {
|
|
736
|
+
handleDpopCheckFailure(ignoreTargetFailure, "target", error);
|
|
699
737
|
}
|
|
700
738
|
if (typeof nonce === "string" && (nonce.length > DPOP_NONCE_MAX_BYTES || Buffer.byteLength(nonce, "utf8") > DPOP_NONCE_MAX_BYTES)) {
|
|
701
739
|
throw invalidProof("DPoP signed nonce exceeds its byte bound.");
|
|
@@ -2309,8 +2347,8 @@ var createDpopReplayPolicy = (options) => {
|
|
|
2309
2347
|
throw invalidProof2("DPoP verifier did not return a verified key binding.");
|
|
2310
2348
|
}
|
|
2311
2349
|
const { type, jkt, alg } = verifiedBinding;
|
|
2312
|
-
if (type !== "dpop" || !isCanonicalDpopJkt(jkt) || !isDpopAlgorithm(alg) || !policy.algorithms.includes(alg) || !isPrintableIdentifier(jti)) {
|
|
2313
|
-
throw invalidProof2("DPoP verified key/algorithm or signed JTI shape is invalid.");
|
|
2350
|
+
if (type !== "dpop" || !isCanonicalDpopJkt(jkt) || !isDpopAlgorithm(alg) || !policy.algorithms.includes(alg) || !isPrintableIdentifier(jti) || typeof iat !== "number" || !Number.isSafeInteger(iat) || iat < 0) {
|
|
2351
|
+
throw invalidProof2("DPoP verified key/algorithm or signed iat/JTI shape is invalid.");
|
|
2314
2352
|
}
|
|
2315
2353
|
const binding = Object.freeze({ type, jkt, alg });
|
|
2316
2354
|
let checkedNow;
|
|
@@ -2318,10 +2356,20 @@ var createDpopReplayPolicy = (options) => {
|
|
|
2318
2356
|
checkedNow = now();
|
|
2319
2357
|
if (!Number.isSafeInteger(checkedNow) || checkedNow < 0) throw new TypeError("DPoP replay clock is invalid.");
|
|
2320
2358
|
} catch (error) {
|
|
2321
|
-
|
|
2359
|
+
handleDpopCheckFailure(policy.ignoreFreshnessFailure, "freshness", replayUnavailable(error));
|
|
2360
|
+
checkedNow = Date.now();
|
|
2361
|
+
if (!Number.isSafeInteger(checkedNow) || checkedNow < 0)
|
|
2362
|
+
throw replayUnavailable(new TypeError("DPoP fallback clock is invalid."));
|
|
2363
|
+
}
|
|
2364
|
+
let expiresAt = getDpopReplayExpiresAt({ ...policy, iat, now: checkedNow });
|
|
2365
|
+
if (expiresAt === null) {
|
|
2366
|
+
handleDpopCheckFailure(
|
|
2367
|
+
policy.ignoreFreshnessFailure,
|
|
2368
|
+
"freshness",
|
|
2369
|
+
invalidProof2("DPoP signed iat is outside the bounded replay window.")
|
|
2370
|
+
);
|
|
2371
|
+
expiresAt = checkedNow + (policy.proofMaxAgeSeconds + policy.clockSkewSeconds) * 1e3;
|
|
2322
2372
|
}
|
|
2323
|
-
const expiresAt = getDpopReplayExpiresAt({ ...policy, iat, now: checkedNow });
|
|
2324
|
-
if (expiresAt === null) throw invalidProof2("DPoP signed iat is outside the bounded replay window.");
|
|
2325
2373
|
if (noncePolicy) {
|
|
2326
2374
|
if (!noncePolicy.verify(nonce, jkt, checkedNow)) {
|
|
2327
2375
|
return Object.freeze({ type: "nonce-challenge", nonce: noncePolicy.issue(jkt, checkedNow) });
|
|
@@ -2329,18 +2377,19 @@ var createDpopReplayPolicy = (options) => {
|
|
|
2329
2377
|
} else if (nonce !== void 0 && (typeof nonce !== "string" || nonce.length > DPOP_NONCE_MAX_BYTES || Buffer.byteLength(nonce, "utf8") > DPOP_NONCE_MAX_BYTES)) {
|
|
2330
2378
|
throw invalidProof2("DPoP signed nonce claim exceeds its string/byte bound.");
|
|
2331
2379
|
}
|
|
2332
|
-
let
|
|
2380
|
+
let reservationError;
|
|
2333
2381
|
try {
|
|
2334
|
-
reserved = await replayStore.reserveDpopProof({
|
|
2382
|
+
const reserved = await replayStore.reserveDpopProof({
|
|
2335
2383
|
replayKey: createDpopReplayKey(effectiveNamespace, jkt, jti),
|
|
2336
2384
|
expiresAt
|
|
2337
2385
|
});
|
|
2338
2386
|
if (typeof reserved !== "boolean")
|
|
2339
2387
|
throw new TypeError("DPoP replay store returned a non-boolean admission result.");
|
|
2388
|
+
if (!reserved) reservationError = invalidProof2("DPoP replay reservation was duplicate or no longer live.");
|
|
2340
2389
|
} catch (error) {
|
|
2341
|
-
|
|
2390
|
+
reservationError = replayUnavailable(error);
|
|
2342
2391
|
}
|
|
2343
|
-
if (
|
|
2392
|
+
if (reservationError) handleDpopCheckFailure(policy.ignoreReplayFailure, "replay", reservationError);
|
|
2344
2393
|
return Object.freeze({ type: "accepted", binding, expiresAt });
|
|
2345
2394
|
}
|
|
2346
2395
|
});
|
|
@@ -2403,12 +2452,11 @@ var createVaultRouteProofVerifier = (options, config, backendOrigin, basePath) =
|
|
|
2403
2452
|
now: options.now
|
|
2404
2453
|
});
|
|
2405
2454
|
const admit = async (request, proof, expectedJkt) => {
|
|
2406
|
-
const targetUrl = targetFor(request.originalUrl);
|
|
2407
2455
|
const result = await replay.verifyAndReserve(
|
|
2408
2456
|
() => verifyDpopProof({
|
|
2409
2457
|
proof,
|
|
2410
2458
|
method: request.method,
|
|
2411
|
-
targetUrl,
|
|
2459
|
+
targetUrl: () => targetFor(request.originalUrl),
|
|
2412
2460
|
proofOptions: policy,
|
|
2413
2461
|
expectedJkt
|
|
2414
2462
|
})
|
|
@@ -2589,12 +2637,11 @@ function createOidcVaultAccessTokenMiddleware(options) {
|
|
|
2589
2637
|
if (!proof && confirmation)
|
|
2590
2638
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_DPOP_REQUIRED", "DPoP authentication is required.");
|
|
2591
2639
|
if (proof) {
|
|
2592
|
-
const targetUrl = targetFor(originalUrl);
|
|
2593
2640
|
const admission = await replay.verifyAndReserve(
|
|
2594
2641
|
() => verifyDpopProof({
|
|
2595
2642
|
proof,
|
|
2596
2643
|
method,
|
|
2597
|
-
targetUrl,
|
|
2644
|
+
targetUrl: () => targetFor(originalUrl),
|
|
2598
2645
|
proofOptions: policy,
|
|
2599
2646
|
accessToken: token,
|
|
2600
2647
|
expectedJkt: confirmation?.jkt
|
package/index.mjs
CHANGED
|
@@ -242,6 +242,26 @@ var parseSelectedCookieValue = (headerValue, selectedName) => {
|
|
|
242
242
|
|
|
243
243
|
// src/device-binding-policy.ts
|
|
244
244
|
import { isIP } from "net";
|
|
245
|
+
|
|
246
|
+
// src/dpop-failure-policy.ts
|
|
247
|
+
var CHECKS = {
|
|
248
|
+
target: { step: 6, option: "ignoreTargetFailure", env: "OIDC_VAULT_DPOP_IGNORE_TARGET_FAILURE" },
|
|
249
|
+
freshness: { step: 7, option: "ignoreFreshnessFailure", env: "OIDC_VAULT_DPOP_IGNORE_FRESHNESS_FAILURE" },
|
|
250
|
+
replay: { step: 8, option: "ignoreReplayFailure", env: "OIDC_VAULT_DPOP_IGNORE_REPLAY_FAILURE" }
|
|
251
|
+
};
|
|
252
|
+
var resolveDpopFailureOption = (value, check) => {
|
|
253
|
+
const { option, env } = CHECKS[check];
|
|
254
|
+
if (value === void 0) return process.env[env] === "true";
|
|
255
|
+
if (typeof value !== "boolean") throw new TypeError(`deviceBinding.${option} must be a boolean.`);
|
|
256
|
+
return value;
|
|
257
|
+
};
|
|
258
|
+
var handleDpopCheckFailure = (ignore, check, error) => {
|
|
259
|
+
if (!ignore) throw error;
|
|
260
|
+
const { step, option, env } = CHECKS[check];
|
|
261
|
+
console.warn(`[express-oidc-vault] DPoP step ${step} (${check}) failed; continuing (${option}=true; ${env}).`, error);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
// src/device-binding-policy.ts
|
|
245
265
|
var isPlainRecord = (value) => {
|
|
246
266
|
if (typeof value !== "object" || value === null) return false;
|
|
247
267
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -258,7 +278,16 @@ var resolveInteger = (value, fallback, min, max, name) => {
|
|
|
258
278
|
var resolveDeviceBindingOptions = (value) => {
|
|
259
279
|
if (value === void 0) return void 0;
|
|
260
280
|
if (!isPlainRecord(value)) throw new Error("deviceBinding must be a plain options object.");
|
|
261
|
-
const {
|
|
281
|
+
const {
|
|
282
|
+
mode,
|
|
283
|
+
algorithms,
|
|
284
|
+
proofMaxAgeSeconds,
|
|
285
|
+
clockSkewSeconds,
|
|
286
|
+
nonce,
|
|
287
|
+
ignoreTargetFailure,
|
|
288
|
+
ignoreFreshnessFailure,
|
|
289
|
+
ignoreReplayFailure
|
|
290
|
+
} = value;
|
|
262
291
|
if (mode !== void 0 && mode !== "optional" && mode !== "required") {
|
|
263
292
|
throw new Error("deviceBinding.mode must be optional or required.");
|
|
264
293
|
}
|
|
@@ -290,7 +319,10 @@ var resolveDeviceBindingOptions = (value) => {
|
|
|
290
319
|
algorithms: Object.freeze([...new Set(allowlist)]),
|
|
291
320
|
proofMaxAgeSeconds: resolveInteger(proofMaxAgeSeconds, 60, 1, 300, "deviceBinding.proofMaxAgeSeconds"),
|
|
292
321
|
clockSkewSeconds: resolveInteger(clockSkewSeconds, 5, 0, 30, "deviceBinding.clockSkewSeconds"),
|
|
293
|
-
nonce: resolvedNonce
|
|
322
|
+
nonce: resolvedNonce,
|
|
323
|
+
ignoreTargetFailure: resolveDpopFailureOption(ignoreTargetFailure, "target"),
|
|
324
|
+
ignoreFreshnessFailure: resolveDpopFailureOption(ignoreFreshnessFailure, "freshness"),
|
|
325
|
+
ignoreReplayFailure: resolveDpopFailureOption(ignoreReplayFailure, "replay")
|
|
294
326
|
});
|
|
295
327
|
};
|
|
296
328
|
var DEVICE_BINDING_STORE_CAPABILITIES = [
|
|
@@ -621,6 +653,7 @@ var publicJwk = (value, alg) => {
|
|
|
621
653
|
var verifyDpopProof = async (input) => {
|
|
622
654
|
const { proof, method, targetUrl, proofOptions, expectedJkt, accessToken } = input;
|
|
623
655
|
const algorithms = [...proofOptions.algorithms];
|
|
656
|
+
const { ignoreTargetFailure } = proofOptions;
|
|
624
657
|
extractDpopProof({ count: 1, value: proof });
|
|
625
658
|
const [encodedHeader, encodedPayload, encodedSignature] = proof.split(".");
|
|
626
659
|
const header = parseObject(decodeBase64url2(encodedHeader, DPOP_PROTECTED_HEADER_MAX_BYTES));
|
|
@@ -646,8 +679,13 @@ var verifyDpopProof = async (input) => {
|
|
|
646
679
|
if (typeof iat !== "number" || !Number.isSafeInteger(iat) || iat < 0 || typeof jti !== "string" || jti.length < 1 || jti.length > 128 || /[^\x20-\x7e]/.test(jti)) {
|
|
647
680
|
throw invalidProof("DPoP signed iat/JTI must be nonnegative integer time and bounded printable ASCII.");
|
|
648
681
|
}
|
|
649
|
-
|
|
650
|
-
|
|
682
|
+
try {
|
|
683
|
+
const expectedTarget = typeof targetUrl === "function" ? targetUrl() : targetUrl;
|
|
684
|
+
if (htm !== method || typeof method !== "string" || method.length === 0 || normalizeDpopProofTarget(htu) !== expectedTarget) {
|
|
685
|
+
throw invalidProof("DPoP signed method or normalized public target does not match this request.");
|
|
686
|
+
}
|
|
687
|
+
} catch (error) {
|
|
688
|
+
handleDpopCheckFailure(ignoreTargetFailure, "target", error);
|
|
651
689
|
}
|
|
652
690
|
if (typeof nonce === "string" && (nonce.length > DPOP_NONCE_MAX_BYTES || Buffer.byteLength(nonce, "utf8") > DPOP_NONCE_MAX_BYTES)) {
|
|
653
691
|
throw invalidProof("DPoP signed nonce exceeds its byte bound.");
|
|
@@ -2261,8 +2299,8 @@ var createDpopReplayPolicy = (options) => {
|
|
|
2261
2299
|
throw invalidProof2("DPoP verifier did not return a verified key binding.");
|
|
2262
2300
|
}
|
|
2263
2301
|
const { type, jkt, alg } = verifiedBinding;
|
|
2264
|
-
if (type !== "dpop" || !isCanonicalDpopJkt(jkt) || !isDpopAlgorithm(alg) || !policy.algorithms.includes(alg) || !isPrintableIdentifier(jti)) {
|
|
2265
|
-
throw invalidProof2("DPoP verified key/algorithm or signed JTI shape is invalid.");
|
|
2302
|
+
if (type !== "dpop" || !isCanonicalDpopJkt(jkt) || !isDpopAlgorithm(alg) || !policy.algorithms.includes(alg) || !isPrintableIdentifier(jti) || typeof iat !== "number" || !Number.isSafeInteger(iat) || iat < 0) {
|
|
2303
|
+
throw invalidProof2("DPoP verified key/algorithm or signed iat/JTI shape is invalid.");
|
|
2266
2304
|
}
|
|
2267
2305
|
const binding = Object.freeze({ type, jkt, alg });
|
|
2268
2306
|
let checkedNow;
|
|
@@ -2270,10 +2308,20 @@ var createDpopReplayPolicy = (options) => {
|
|
|
2270
2308
|
checkedNow = now();
|
|
2271
2309
|
if (!Number.isSafeInteger(checkedNow) || checkedNow < 0) throw new TypeError("DPoP replay clock is invalid.");
|
|
2272
2310
|
} catch (error) {
|
|
2273
|
-
|
|
2311
|
+
handleDpopCheckFailure(policy.ignoreFreshnessFailure, "freshness", replayUnavailable(error));
|
|
2312
|
+
checkedNow = Date.now();
|
|
2313
|
+
if (!Number.isSafeInteger(checkedNow) || checkedNow < 0)
|
|
2314
|
+
throw replayUnavailable(new TypeError("DPoP fallback clock is invalid."));
|
|
2315
|
+
}
|
|
2316
|
+
let expiresAt = getDpopReplayExpiresAt({ ...policy, iat, now: checkedNow });
|
|
2317
|
+
if (expiresAt === null) {
|
|
2318
|
+
handleDpopCheckFailure(
|
|
2319
|
+
policy.ignoreFreshnessFailure,
|
|
2320
|
+
"freshness",
|
|
2321
|
+
invalidProof2("DPoP signed iat is outside the bounded replay window.")
|
|
2322
|
+
);
|
|
2323
|
+
expiresAt = checkedNow + (policy.proofMaxAgeSeconds + policy.clockSkewSeconds) * 1e3;
|
|
2274
2324
|
}
|
|
2275
|
-
const expiresAt = getDpopReplayExpiresAt({ ...policy, iat, now: checkedNow });
|
|
2276
|
-
if (expiresAt === null) throw invalidProof2("DPoP signed iat is outside the bounded replay window.");
|
|
2277
2325
|
if (noncePolicy) {
|
|
2278
2326
|
if (!noncePolicy.verify(nonce, jkt, checkedNow)) {
|
|
2279
2327
|
return Object.freeze({ type: "nonce-challenge", nonce: noncePolicy.issue(jkt, checkedNow) });
|
|
@@ -2281,18 +2329,19 @@ var createDpopReplayPolicy = (options) => {
|
|
|
2281
2329
|
} else if (nonce !== void 0 && (typeof nonce !== "string" || nonce.length > DPOP_NONCE_MAX_BYTES || Buffer.byteLength(nonce, "utf8") > DPOP_NONCE_MAX_BYTES)) {
|
|
2282
2330
|
throw invalidProof2("DPoP signed nonce claim exceeds its string/byte bound.");
|
|
2283
2331
|
}
|
|
2284
|
-
let
|
|
2332
|
+
let reservationError;
|
|
2285
2333
|
try {
|
|
2286
|
-
reserved = await replayStore.reserveDpopProof({
|
|
2334
|
+
const reserved = await replayStore.reserveDpopProof({
|
|
2287
2335
|
replayKey: createDpopReplayKey(effectiveNamespace, jkt, jti),
|
|
2288
2336
|
expiresAt
|
|
2289
2337
|
});
|
|
2290
2338
|
if (typeof reserved !== "boolean")
|
|
2291
2339
|
throw new TypeError("DPoP replay store returned a non-boolean admission result.");
|
|
2340
|
+
if (!reserved) reservationError = invalidProof2("DPoP replay reservation was duplicate or no longer live.");
|
|
2292
2341
|
} catch (error) {
|
|
2293
|
-
|
|
2342
|
+
reservationError = replayUnavailable(error);
|
|
2294
2343
|
}
|
|
2295
|
-
if (
|
|
2344
|
+
if (reservationError) handleDpopCheckFailure(policy.ignoreReplayFailure, "replay", reservationError);
|
|
2296
2345
|
return Object.freeze({ type: "accepted", binding, expiresAt });
|
|
2297
2346
|
}
|
|
2298
2347
|
});
|
|
@@ -2355,12 +2404,11 @@ var createVaultRouteProofVerifier = (options, config, backendOrigin, basePath) =
|
|
|
2355
2404
|
now: options.now
|
|
2356
2405
|
});
|
|
2357
2406
|
const admit = async (request, proof, expectedJkt) => {
|
|
2358
|
-
const targetUrl = targetFor(request.originalUrl);
|
|
2359
2407
|
const result = await replay.verifyAndReserve(
|
|
2360
2408
|
() => verifyDpopProof({
|
|
2361
2409
|
proof,
|
|
2362
2410
|
method: request.method,
|
|
2363
|
-
targetUrl,
|
|
2411
|
+
targetUrl: () => targetFor(request.originalUrl),
|
|
2364
2412
|
proofOptions: policy,
|
|
2365
2413
|
expectedJkt
|
|
2366
2414
|
})
|
|
@@ -2541,12 +2589,11 @@ function createOidcVaultAccessTokenMiddleware(options) {
|
|
|
2541
2589
|
if (!proof && confirmation)
|
|
2542
2590
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_DPOP_REQUIRED", "DPoP authentication is required.");
|
|
2543
2591
|
if (proof) {
|
|
2544
|
-
const targetUrl = targetFor(originalUrl);
|
|
2545
2592
|
const admission = await replay.verifyAndReserve(
|
|
2546
2593
|
() => verifyDpopProof({
|
|
2547
2594
|
proof,
|
|
2548
2595
|
method,
|
|
2549
|
-
targetUrl,
|
|
2596
|
+
targetUrl: () => targetFor(originalUrl),
|
|
2550
2597
|
proofOptions: policy,
|
|
2551
2598
|
accessToken: token,
|
|
2552
2599
|
expectedJkt: confirmation?.jkt
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@web-ts-toolkit/express-oidc-vault",
|
|
3
3
|
"description": "Express OIDC sessions with body or cookie transport, optional DPoP-bound local JWTs, and pluggable vault stores",
|
|
4
4
|
"homepage": "https://web-ts-toolkit.pages.dev/docs/packages/express-oidc-vault",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.51.0",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"keywords": [
|
|
8
8
|
"express",
|