@web-ts-toolkit/express-oidc-vault 0.53.0 → 0.54.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 +416 -45
- package/index.d.mts +200 -8
- package/index.d.ts +200 -8
- package/index.js +992 -186
- package/index.mjs +988 -187
- package/package.json +1 -1
- package/wire.d.mts +11 -1
- package/wire.d.ts +11 -1
package/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { createHash as
|
|
2
|
+
import { createHash as createHash8, randomBytes as randomBytes4 } from "crypto";
|
|
3
3
|
import express from "express";
|
|
4
4
|
|
|
5
5
|
// src/errors.ts
|
|
@@ -97,6 +97,7 @@ var OIDC_VAULT_ROUTE_PATHS = {
|
|
|
97
97
|
exchange: "/exchange",
|
|
98
98
|
refresh: "/refresh",
|
|
99
99
|
logout: "/logout",
|
|
100
|
+
"logout-navigation": "/logout/navigation",
|
|
100
101
|
"backchannel-logout": "/backchannel-logout"
|
|
101
102
|
};
|
|
102
103
|
|
|
@@ -570,10 +571,11 @@ var invalidProof = (diagnostic, cause) => {
|
|
|
570
571
|
if (cause !== void 0) Object.defineProperty(error, "cause", { value: cause, configurable: true });
|
|
571
572
|
return error;
|
|
572
573
|
};
|
|
573
|
-
var extractDpopProof = (header) => {
|
|
574
|
+
var extractDpopProof = (header, diagnostic) => {
|
|
574
575
|
if (header.count === 0) return void 0;
|
|
575
576
|
const { value } = header;
|
|
576
577
|
if (header.count !== 1 || typeof value !== "string" || value.length === 0 || value.length > DPOP_PROOF_MAX_BYTES || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value)) {
|
|
578
|
+
diagnostic?.emit("presentation", "rejected", "malformed-proof");
|
|
577
579
|
throw invalidProof("DPoP header must contain one bounded compact signed proof.");
|
|
578
580
|
}
|
|
579
581
|
return value;
|
|
@@ -651,58 +653,105 @@ var publicJwk = (value, alg) => {
|
|
|
651
653
|
return value;
|
|
652
654
|
};
|
|
653
655
|
var verifyDpopProof = async (input) => {
|
|
654
|
-
const { proof, method, targetUrl, proofOptions, expectedJkt, accessToken } = input;
|
|
656
|
+
const { proof, method, targetUrl, proofOptions, expectedJkt, accessToken, diagnostic } = input;
|
|
655
657
|
const algorithms = [...proofOptions.algorithms];
|
|
656
658
|
const { ignoreTargetFailure } = proofOptions;
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const header = parseObject(decodeBase64url2(encodedHeader, DPOP_PROTECTED_HEADER_MAX_BYTES));
|
|
660
|
-
decodeBase64url2(encodedPayload, DPOP_PROOF_MAX_BYTES);
|
|
661
|
-
const signature = decodeBase64url2(encodedSignature, 512);
|
|
662
|
-
const { alg } = header;
|
|
663
|
-
if (header.typ !== "dpop+jwt" || !isDpopAlgorithm(alg) || !algorithms.includes(alg) || ["crit", "b64", "jku", "x5u", "x5c", "x5t", "x5t#S256"].some((key) => Object.hasOwn(header, key))) {
|
|
664
|
-
throw invalidProof("DPoP protected header has an unsupported type, algorithm or key/critical extension.");
|
|
665
|
-
}
|
|
666
|
-
const jwk = publicJwk(header.jwk, alg);
|
|
667
|
-
const signatureBytes = alg === "ES256" ? 64 : decodeBase64url2(jwk.n, 512).length;
|
|
668
|
-
if (signature.length !== signatureBytes)
|
|
669
|
-
throw invalidProof("DPoP signature length does not match the allowed key profile.");
|
|
670
|
-
let payload;
|
|
671
|
-
try {
|
|
672
|
-
const key = await importJWK(jwk, alg);
|
|
673
|
-
({ payload } = await compactVerify(proof, key, { algorithms }));
|
|
674
|
-
} catch (cause) {
|
|
675
|
-
throw invalidProof("DPoP public-key import or signature verification failed.", cause);
|
|
676
|
-
}
|
|
677
|
-
const claims = parseObject(payload);
|
|
678
|
-
const { htm, htu, iat, jti, nonce, ath } = claims;
|
|
679
|
-
if (typeof iat !== "number" || !Number.isSafeInteger(iat) || iat < 0 || typeof jti !== "string" || jti.length < 1 || jti.length > 128 || /[^\x20-\x7e]/.test(jti)) {
|
|
680
|
-
throw invalidProof("DPoP signed iat/JTI must be nonnegative integer time and bounded printable ASCII.");
|
|
681
|
-
}
|
|
659
|
+
let stage = "header";
|
|
660
|
+
let reason = "malformed-proof";
|
|
682
661
|
try {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
662
|
+
extractDpopProof({ count: 1, value: proof }, diagnostic);
|
|
663
|
+
const [encodedHeader, encodedPayload, encodedSignature] = proof.split(".");
|
|
664
|
+
const header = parseObject(decodeBase64url2(encodedHeader, DPOP_PROTECTED_HEADER_MAX_BYTES));
|
|
665
|
+
decodeBase64url2(encodedPayload, DPOP_PROOF_MAX_BYTES);
|
|
666
|
+
const signature = decodeBase64url2(encodedSignature, 512);
|
|
667
|
+
const { alg } = header;
|
|
668
|
+
reason = "unsupported-header";
|
|
669
|
+
if (header.typ !== "dpop+jwt" || !isDpopAlgorithm(alg) || !algorithms.includes(alg) || ["crit", "b64", "jku", "x5u", "x5c", "x5t", "x5t#S256"].some((key) => Object.hasOwn(header, key))) {
|
|
670
|
+
throw invalidProof("DPoP protected header has an unsupported type, algorithm or key/critical extension.");
|
|
671
|
+
}
|
|
672
|
+
diagnostic?.emit("header", "accepted", "check-passed");
|
|
673
|
+
stage = "key";
|
|
674
|
+
reason = "invalid-key";
|
|
675
|
+
const jwk = publicJwk(header.jwk, alg);
|
|
676
|
+
stage = "signature";
|
|
677
|
+
reason = "invalid-signature";
|
|
678
|
+
const signatureBytes = alg === "ES256" ? 64 : decodeBase64url2(jwk.n, 512).length;
|
|
679
|
+
if (signature.length !== signatureBytes)
|
|
680
|
+
throw invalidProof("DPoP signature length does not match the allowed key profile.");
|
|
681
|
+
let payload;
|
|
682
|
+
try {
|
|
683
|
+
stage = "key";
|
|
684
|
+
reason = "invalid-key";
|
|
685
|
+
const key = await importJWK(jwk, alg);
|
|
686
|
+
diagnostic?.emit("key", "accepted", "check-passed");
|
|
687
|
+
stage = "signature";
|
|
688
|
+
reason = "invalid-signature";
|
|
689
|
+
({ payload } = await compactVerify(proof, key, { algorithms }));
|
|
690
|
+
} catch (cause) {
|
|
691
|
+
throw invalidProof("DPoP public-key import or signature verification failed.", cause);
|
|
692
|
+
}
|
|
693
|
+
diagnostic?.mark({ signature: "verified" });
|
|
694
|
+
diagnostic?.emit("signature", "accepted", "check-passed");
|
|
695
|
+
stage = "claims";
|
|
696
|
+
reason = "invalid-claims";
|
|
697
|
+
const claims = parseObject(payload);
|
|
698
|
+
const { htm, htu, iat, jti, nonce, ath } = claims;
|
|
699
|
+
if (typeof iat !== "number" || !Number.isSafeInteger(iat) || iat < 0 || typeof jti !== "string" || jti.length < 1 || jti.length > 128 || /[^\x20-\x7e]/.test(jti)) {
|
|
700
|
+
throw invalidProof("DPoP signed iat/JTI must be nonnegative integer time and bounded printable ASCII.");
|
|
701
|
+
}
|
|
702
|
+
diagnostic?.signedTime(iat);
|
|
703
|
+
diagnostic?.emit("claims", "accepted", "check-passed");
|
|
704
|
+
stage = "target";
|
|
705
|
+
reason = "target-mismatch";
|
|
706
|
+
try {
|
|
707
|
+
const expectedTarget = typeof targetUrl === "function" ? targetUrl() : targetUrl;
|
|
708
|
+
if (htm !== method || typeof method !== "string" || method.length === 0 || normalizeDpopProofTarget(htu) !== expectedTarget) {
|
|
709
|
+
throw invalidProof("DPoP signed method or normalized public target does not match this request.");
|
|
710
|
+
}
|
|
711
|
+
diagnostic?.mark({ target: "matched" });
|
|
712
|
+
diagnostic?.emit("target", "accepted", "check-passed");
|
|
713
|
+
} catch (error) {
|
|
714
|
+
if (ignoreTargetFailure) {
|
|
715
|
+
diagnostic?.mark({ target: "ignored" });
|
|
716
|
+
diagnostic?.emit("target", "ignored", reason);
|
|
717
|
+
}
|
|
718
|
+
handleDpopCheckFailure(ignoreTargetFailure, "target", error);
|
|
719
|
+
}
|
|
720
|
+
stage = "nonce";
|
|
721
|
+
reason = "invalid-nonce";
|
|
722
|
+
if (typeof nonce === "string" && (nonce.length > DPOP_NONCE_MAX_BYTES || Buffer.byteLength(nonce, "utf8") > DPOP_NONCE_MAX_BYTES)) {
|
|
723
|
+
throw invalidProof("DPoP signed nonce exceeds its byte bound.");
|
|
686
724
|
}
|
|
725
|
+
stage = "token-hash";
|
|
726
|
+
reason = "token-hash-mismatch";
|
|
727
|
+
if (accessToken !== void 0 && (typeof accessToken !== "string" || !/^[\x21-\x7e]+$/.test(accessToken) || !isCanonicalDpopJkt(ath) || ath !== createHash2("sha256").update(accessToken, "ascii").digest("base64url"))) {
|
|
728
|
+
throw invalidProof("DPoP signed access-token hash does not match the presented API token.");
|
|
729
|
+
}
|
|
730
|
+
diagnostic?.mark({ tokenHash: accessToken === void 0 ? "not-applicable" : "matched" });
|
|
731
|
+
if (accessToken !== void 0) diagnostic?.emit("token-hash", "accepted", "check-passed");
|
|
732
|
+
stage = "binding";
|
|
733
|
+
reason = "key-mismatch";
|
|
734
|
+
const jkt = await calculateJwkThumbprint(jwk, "sha256");
|
|
735
|
+
if (expectedJkt !== void 0 && (!isCanonicalDpopJkt(expectedJkt) || jkt !== expectedJkt)) {
|
|
736
|
+
throw invalidProof("DPoP verified public key does not match the original credential confirmation.");
|
|
737
|
+
}
|
|
738
|
+
diagnostic?.mark({ credentialBinding: expectedJkt === void 0 ? "not-applicable" : "matched" });
|
|
739
|
+
if (expectedJkt !== void 0) diagnostic?.emit("binding", "accepted", "check-passed");
|
|
740
|
+
return Object.freeze({
|
|
741
|
+
binding: Object.freeze({ type: "dpop", jkt, alg }),
|
|
742
|
+
iat,
|
|
743
|
+
jti,
|
|
744
|
+
...nonce === void 0 ? {} : { nonce }
|
|
745
|
+
});
|
|
687
746
|
} catch (error) {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
throw
|
|
695
|
-
}
|
|
696
|
-
const jkt = await calculateJwkThumbprint(jwk, "sha256");
|
|
697
|
-
if (expectedJkt !== void 0 && (!isCanonicalDpopJkt(expectedJkt) || jkt !== expectedJkt)) {
|
|
698
|
-
throw invalidProof("DPoP verified public key does not match the original credential confirmation.");
|
|
747
|
+
if (stage === "signature") diagnostic?.mark({ signature: "failed" });
|
|
748
|
+
else if (stage === "target") diagnostic?.mark({ target: "failed" });
|
|
749
|
+
else if (stage === "token-hash") diagnostic?.mark({ tokenHash: "failed" });
|
|
750
|
+
else if (stage === "binding") diagnostic?.mark({ credentialBinding: "failed" });
|
|
751
|
+
else if (stage === "nonce") diagnostic?.mark({ nonce: "failed" });
|
|
752
|
+
diagnostic?.emit(stage, "rejected", reason);
|
|
753
|
+
throw error;
|
|
699
754
|
}
|
|
700
|
-
return Object.freeze({
|
|
701
|
-
binding: Object.freeze({ type: "dpop", jkt, alg }),
|
|
702
|
-
iat,
|
|
703
|
-
jti,
|
|
704
|
-
...nonce === void 0 ? {} : { nonce }
|
|
705
|
-
});
|
|
706
755
|
};
|
|
707
756
|
|
|
708
757
|
// src/fingerprint-recognition.ts
|
|
@@ -765,11 +814,11 @@ var snapshotFingerprintRecognition = (metadata) => {
|
|
|
765
814
|
const value = metadata?.[FINGERPRINT_RECOGNITION_METADATA_KEY];
|
|
766
815
|
if (value === void 0) return void 0;
|
|
767
816
|
if (!isPlainRecord2(value)) throw new TypeError("Stored fingerprint recognition metadata is invalid.");
|
|
768
|
-
const { version, hash } = value;
|
|
769
|
-
if (Object.keys(value).length !== 2 || !Object.hasOwn(value, "version") || !Object.hasOwn(value, "hash") || version !== 1 || !isCanonicalDpopJkt(
|
|
817
|
+
const { version, hash: hash2 } = value;
|
|
818
|
+
if (Object.keys(value).length !== 2 || !Object.hasOwn(value, "version") || !Object.hasOwn(value, "hash") || version !== 1 || !isCanonicalDpopJkt(hash2)) {
|
|
770
819
|
throw new TypeError("Stored fingerprint recognition metadata is invalid.");
|
|
771
820
|
}
|
|
772
|
-
return Object.freeze({ version, hash });
|
|
821
|
+
return Object.freeze({ version, hash: hash2 });
|
|
773
822
|
};
|
|
774
823
|
var fingerprintRecognitionMatches = (actual, original) => actual?.hash === original?.hash;
|
|
775
824
|
var assertFingerprintRecognition = (policy, original, supplied) => {
|
|
@@ -1105,6 +1154,249 @@ function resolveOidcVaultConfigFromEnv(env = process.env) {
|
|
|
1105
1154
|
});
|
|
1106
1155
|
}
|
|
1107
1156
|
|
|
1157
|
+
// src/diagnostics.ts
|
|
1158
|
+
import { createHash as createHash5 } from "crypto";
|
|
1159
|
+
var createDiagnosticDispatcher = (observer) => {
|
|
1160
|
+
let pending = 0;
|
|
1161
|
+
let invoking = false;
|
|
1162
|
+
return (event) => {
|
|
1163
|
+
if (!observer || invoking || pending >= 32) return;
|
|
1164
|
+
invoking = true;
|
|
1165
|
+
try {
|
|
1166
|
+
const result = observer(event);
|
|
1167
|
+
if (result !== void 0) {
|
|
1168
|
+
pending++;
|
|
1169
|
+
const release = () => {
|
|
1170
|
+
pending--;
|
|
1171
|
+
};
|
|
1172
|
+
void Promise.resolve(result).then(release, release);
|
|
1173
|
+
}
|
|
1174
|
+
} catch {
|
|
1175
|
+
} finally {
|
|
1176
|
+
invoking = false;
|
|
1177
|
+
}
|
|
1178
|
+
};
|
|
1179
|
+
};
|
|
1180
|
+
var reference = (purpose, value) => typeof value === "string" && value.length > 0 && value.length <= 1024 ? createHash5("sha256").update(`${purpose}\0${value}`, "utf8").digest("base64url") : void 0;
|
|
1181
|
+
var diagnosticCorrelation = (association, subject, sessionId) => {
|
|
1182
|
+
const subjectRef = reference("subject", subject);
|
|
1183
|
+
const sessionRef = reference("session", sessionId);
|
|
1184
|
+
return Object.freeze({ association, ...subjectRef ? { subjectRef } : {}, ...sessionRef ? { sessionRef } : {} });
|
|
1185
|
+
};
|
|
1186
|
+
var DpopDiagnostic = class {
|
|
1187
|
+
constructor(operation, dispatch) {
|
|
1188
|
+
this.operation = operation;
|
|
1189
|
+
this.dispatch = dispatch;
|
|
1190
|
+
this.verification = {
|
|
1191
|
+
signature: "unchecked",
|
|
1192
|
+
target: "unchecked",
|
|
1193
|
+
tokenHash: "unchecked",
|
|
1194
|
+
credentialBinding: "unchecked",
|
|
1195
|
+
freshness: "unchecked",
|
|
1196
|
+
nonce: "unchecked",
|
|
1197
|
+
replay: "unchecked"
|
|
1198
|
+
};
|
|
1199
|
+
this.terminal = false;
|
|
1200
|
+
}
|
|
1201
|
+
associate(correlation) {
|
|
1202
|
+
this.correlation = correlation;
|
|
1203
|
+
}
|
|
1204
|
+
mark(update) {
|
|
1205
|
+
this.verification = { ...this.verification, ...update };
|
|
1206
|
+
}
|
|
1207
|
+
signedTime(iatSeconds) {
|
|
1208
|
+
this.timing = Object.freeze({ provenance: "signature-verified", iatSeconds });
|
|
1209
|
+
}
|
|
1210
|
+
checkTime(checkedAtMs, clock) {
|
|
1211
|
+
if (!this.timing) return;
|
|
1212
|
+
const issuedAt = this.timing.iatSeconds * 1e3;
|
|
1213
|
+
const age = checkedAtMs - issuedAt;
|
|
1214
|
+
this.timing = Object.freeze({
|
|
1215
|
+
...this.timing,
|
|
1216
|
+
clock,
|
|
1217
|
+
checkedAtMs,
|
|
1218
|
+
...Number.isSafeInteger(issuedAt) && Number.isSafeInteger(age) ? { observedAgeMs: age } : {}
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
emit(stage, outcome, reason) {
|
|
1222
|
+
if (this.terminal) return;
|
|
1223
|
+
if (outcome === "rejected" || outcome === "challenged" || stage === "proof" || stage === "presentation" && outcome === "ignored")
|
|
1224
|
+
this.terminal = true;
|
|
1225
|
+
this.dispatch(
|
|
1226
|
+
Object.freeze({
|
|
1227
|
+
operation: this.operation,
|
|
1228
|
+
stage,
|
|
1229
|
+
outcome,
|
|
1230
|
+
reason,
|
|
1231
|
+
verification: Object.freeze({ ...this.verification }),
|
|
1232
|
+
...this.timing ? { timing: this.timing } : {},
|
|
1233
|
+
...this.correlation ? { correlation: this.correlation } : {}
|
|
1234
|
+
})
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
accept() {
|
|
1238
|
+
if (this.correlation && this.hasAuthenticatedProof()) {
|
|
1239
|
+
this.correlation = Object.freeze({ ...this.correlation, association: "authenticated" });
|
|
1240
|
+
}
|
|
1241
|
+
this.emit("proof", "accepted", "proof-accepted");
|
|
1242
|
+
}
|
|
1243
|
+
/** Policy acceptance with an ignored security check is not strict identity evidence. */
|
|
1244
|
+
hasAuthenticatedProof() {
|
|
1245
|
+
const v = this.verification;
|
|
1246
|
+
return v.signature === "verified" && v.target === "matched" && v.credentialBinding === "matched" && (v.tokenHash === "matched" || v.tokenHash === "not-applicable") && v.freshness === "passed" && (v.nonce === "passed" || v.nonce === "disabled") && v.replay === "reserved";
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
var diagnosticPreflightReason = (code) => {
|
|
1250
|
+
switch (code) {
|
|
1251
|
+
case "OIDC_VAULT_DPOP_REQUIRED":
|
|
1252
|
+
return "proof-required";
|
|
1253
|
+
case "OIDC_VAULT_DEVICE_BINDING_REQUIRED":
|
|
1254
|
+
return "binding-required";
|
|
1255
|
+
case "OIDC_VAULT_INVALID_SESSION":
|
|
1256
|
+
return "session-mismatch";
|
|
1257
|
+
case "OIDC_VAULT_INVALID_EXCHANGE_CODE":
|
|
1258
|
+
return "session-unavailable";
|
|
1259
|
+
case "OIDC_VAULT_INVALID_ORIGIN":
|
|
1260
|
+
case "OIDC_VAULT_UNTRUSTED_ORIGIN":
|
|
1261
|
+
return "origin-rejected";
|
|
1262
|
+
case "OIDC_VAULT_INVALID_BROWSER_BINDING":
|
|
1263
|
+
return "browser-binding-rejected";
|
|
1264
|
+
case "OIDC_VAULT_FINGERPRINT_REAUTH_REQUIRED":
|
|
1265
|
+
return "recognition-rejected";
|
|
1266
|
+
default:
|
|
1267
|
+
return "preflight-failed";
|
|
1268
|
+
}
|
|
1269
|
+
};
|
|
1270
|
+
var createDpopDiagnosticFactory = (observer) => {
|
|
1271
|
+
const dispatch = createDiagnosticDispatcher(observer);
|
|
1272
|
+
return (operation) => new DpopDiagnostic(operation, dispatch);
|
|
1273
|
+
};
|
|
1274
|
+
|
|
1275
|
+
// src/logout-navigation.ts
|
|
1276
|
+
import { createHash as createHash6, randomBytes as randomBytes3 } from "crypto";
|
|
1277
|
+
import { isIP as isIP2 } from "net";
|
|
1278
|
+
|
|
1279
|
+
// src/types.ts
|
|
1280
|
+
var OidcVaultStoreConflictError = class extends Error {
|
|
1281
|
+
constructor(message = "OIDC vault store operation conflicted with concurrent state changes.") {
|
|
1282
|
+
super(message);
|
|
1283
|
+
this.name = "OidcVaultStoreConflictError";
|
|
1284
|
+
}
|
|
1285
|
+
};
|
|
1286
|
+
var OidcVaultDpopReplayCapacityError = class extends Error {
|
|
1287
|
+
constructor(message = "OIDC vault DPoP replay store capacity is exhausted.") {
|
|
1288
|
+
super(message);
|
|
1289
|
+
this.name = "OidcVaultDpopReplayCapacityError";
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
var OidcVaultLogoutNavigationCapacityError = class extends Error {
|
|
1293
|
+
constructor(message = "OIDC vault logout navigation ticket store capacity is exhausted.") {
|
|
1294
|
+
super(message);
|
|
1295
|
+
this.name = "OidcVaultLogoutNavigationCapacityError";
|
|
1296
|
+
}
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
// src/logout-navigation.ts
|
|
1300
|
+
var UNSAFE_URL = /[\s\\\u0000-\u001f\u007f-\u009f]/u;
|
|
1301
|
+
var TICKET = /^lnt_[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;
|
|
1302
|
+
var hash = (value) => createHash6("sha256").update(value, "utf8").digest("base64url");
|
|
1303
|
+
var plainRecord = (value) => typeof value === "object" && value !== null && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
1304
|
+
var destination = (value, name, maxBytes, secure) => {
|
|
1305
|
+
const invalid = () => new TypeError(`${name} must be a bounded absolute HTTP(S) URL without unsafe characters, userinfo or fragment.`);
|
|
1306
|
+
if (typeof value !== "string" || !/^https?:\/\//i.test(value) || /^https?:\/\/[^/?#]*@/i.test(value) || UNSAFE_URL.test(value) || value.includes("#") || Buffer.byteLength(value, "utf8") > maxBytes)
|
|
1307
|
+
throw invalid();
|
|
1308
|
+
let url;
|
|
1309
|
+
try {
|
|
1310
|
+
url = new URL(value);
|
|
1311
|
+
} catch {
|
|
1312
|
+
throw invalid();
|
|
1313
|
+
}
|
|
1314
|
+
const loopback = url.hostname === "localhost" || url.hostname === "[::1]" || isIP2(url.hostname) === 4 && url.hostname.startsWith("127.");
|
|
1315
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || secure && url.protocol === "http:" && !loopback || url.username || url.password || url.hash || Buffer.byteLength(url.href, "utf8") > maxBytes)
|
|
1316
|
+
throw invalid();
|
|
1317
|
+
return url;
|
|
1318
|
+
};
|
|
1319
|
+
var resolveLogoutNavigationOptions = (value, input) => {
|
|
1320
|
+
if (value === void 0) return void 0;
|
|
1321
|
+
if (!plainRecord(value)) throw new TypeError("logoutNavigation must be a plain options object.");
|
|
1322
|
+
if (!input.deviceBindingConfigured) throw new TypeError("logoutNavigation requires deviceBinding.");
|
|
1323
|
+
for (const name of ["createLogoutNavigationTicket", "consumeLogoutNavigationTicket"]) {
|
|
1324
|
+
if (typeof input.store[name] !== "function")
|
|
1325
|
+
throw new TypeError(`logoutNavigation requires storeProvider.${name}.`);
|
|
1326
|
+
}
|
|
1327
|
+
const { fallbackRedirectUri, ticketTtlMs = 6e4, namespace = "default" } = value;
|
|
1328
|
+
if (!Number.isInteger(ticketTtlMs) || ticketTtlMs < 1e3 || ticketTtlMs > 12e4)
|
|
1329
|
+
throw new TypeError("logoutNavigation.ticketTtlMs must be an integer from 1000 through 120000.");
|
|
1330
|
+
computeExpiresAt(input.now, ticketTtlMs, "logoutNavigation.ticketTtlMs");
|
|
1331
|
+
if (typeof namespace !== "string" || !/^[\x21-\x7e]{1,128}$/.test(namespace))
|
|
1332
|
+
throw new TypeError("logoutNavigation.namespace must be a static printable ASCII label of 1 through 128 bytes.");
|
|
1333
|
+
const path = `${input.basePath}${OIDC_VAULT_ROUTE_PATHS["logout-navigation"]}`;
|
|
1334
|
+
const fallback = destination(fallbackRedirectUri, "logoutNavigation.fallbackRedirectUri", 2048, true);
|
|
1335
|
+
if (fallback.origin !== input.backendOrigin && (input.frontendRedirectUri === void 0 || fallback.origin !== new URL(input.frontendRedirectUri).origin) || // Express's default mount/route matching is case-insensitive and accepts
|
|
1336
|
+
// a trailing slash. Those aliases are continuation loops as well.
|
|
1337
|
+
fallback.origin === input.backendOrigin && fallback.pathname.replace(/\/$/, "").toLowerCase() === path.toLowerCase())
|
|
1338
|
+
throw new TypeError(
|
|
1339
|
+
"logoutNavigation.fallbackRedirectUri must stay on the backend/frontend origin and cannot loop to logout navigation."
|
|
1340
|
+
);
|
|
1341
|
+
const postLogout = input.postLogoutRedirectUri === void 0 ? void 0 : destination(input.postLogoutRedirectUri, "logoutNavigation.postLogoutRedirectUri", 2048, true).href;
|
|
1342
|
+
return Object.freeze({
|
|
1343
|
+
fallbackRedirectUri: fallback.href,
|
|
1344
|
+
ticketTtlMs,
|
|
1345
|
+
namespace: "logout-navigation:v1:" + hash(
|
|
1346
|
+
JSON.stringify([
|
|
1347
|
+
"oidc-vault-logout-navigation",
|
|
1348
|
+
1,
|
|
1349
|
+
input.backendOrigin,
|
|
1350
|
+
input.basePath,
|
|
1351
|
+
input.config.issuer,
|
|
1352
|
+
input.config.clientId,
|
|
1353
|
+
namespace,
|
|
1354
|
+
fallback.href,
|
|
1355
|
+
postLogout ?? null
|
|
1356
|
+
])
|
|
1357
|
+
),
|
|
1358
|
+
navigationUrl: `${input.backendOrigin}${path}`,
|
|
1359
|
+
...postLogout === void 0 ? {} : { postLogoutRedirectUri: postLogout }
|
|
1360
|
+
});
|
|
1361
|
+
};
|
|
1362
|
+
var validateUpstreamLogoutUrl = (value) => destination(value, "Logout navigation upstream destination", 16384, false).href;
|
|
1363
|
+
var persistLogoutNavigation = async (policy, store, upstreamLogoutUrl, now) => {
|
|
1364
|
+
const expiresAt = computeExpiresAt(now, policy.ticketTtlMs, "logoutNavigation.ticketTtlMs");
|
|
1365
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1366
|
+
const ticket = `lnt_${randomBytes3(32).toString("base64url")}`;
|
|
1367
|
+
try {
|
|
1368
|
+
await store.createLogoutNavigationTicket({
|
|
1369
|
+
namespace: policy.namespace,
|
|
1370
|
+
ticketHash: hash(ticket),
|
|
1371
|
+
upstreamLogoutUrl,
|
|
1372
|
+
createdAt: now,
|
|
1373
|
+
expiresAt
|
|
1374
|
+
});
|
|
1375
|
+
return `${policy.navigationUrl}?ticket=${ticket}`;
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
const conflict = error instanceof OidcVaultStoreConflictError || typeof error === "object" && error !== null && "name" in error && error.name === "OidcVaultStoreConflictError";
|
|
1378
|
+
if (!conflict || attempt === 2) throw error;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
throw new Error("Logout navigation ticket attempts exhausted.");
|
|
1382
|
+
};
|
|
1383
|
+
var navigationTicketHash = (originalUrl) => {
|
|
1384
|
+
const index = originalUrl.indexOf("?");
|
|
1385
|
+
if (index < 0) return void 0;
|
|
1386
|
+
const query = originalUrl.slice(index + 1);
|
|
1387
|
+
if (!query.startsWith("ticket=")) return void 0;
|
|
1388
|
+
const ticket = query.slice("ticket=".length);
|
|
1389
|
+
return TICKET.test(ticket) ? hash(ticket) : void 0;
|
|
1390
|
+
};
|
|
1391
|
+
var consumedLogoutDestination = (value, namespace, ticketHash, now) => {
|
|
1392
|
+
if (!plainRecord(value) || Object.keys(value).length !== 5)
|
|
1393
|
+
throw new TypeError("Logout navigation consume returned an invalid record.");
|
|
1394
|
+
const { namespace: returnedNamespace, ticketHash: returnedHash, upstreamLogoutUrl, createdAt, expiresAt } = value;
|
|
1395
|
+
if (returnedNamespace !== namespace || returnedHash !== ticketHash || !isUsableEpochMs(now) || !isUsableEpochMs(createdAt) || !isUsableEpochMs(expiresAt) || expiresAt - createdAt < 1e3 || expiresAt - createdAt > 12e4 || expiresAt <= now || expiresAt - now > 12e4)
|
|
1396
|
+
throw new TypeError("Logout navigation consume returned invalid identity or lifetime.");
|
|
1397
|
+
return validateUpstreamLogoutUrl(upstreamLogoutUrl);
|
|
1398
|
+
};
|
|
1399
|
+
|
|
1108
1400
|
// src/utils.ts
|
|
1109
1401
|
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
1110
1402
|
var isString = (value) => typeof value === "string" && value.length > 0;
|
|
@@ -1189,8 +1481,8 @@ var getRequestSourceOrigin = (req) => {
|
|
|
1189
1481
|
var assertTrustedOrigin = (req, options, trustedOrigins, action) => {
|
|
1190
1482
|
captureTrustedOriginGuard(req, options, trustedOrigins, action)();
|
|
1191
1483
|
};
|
|
1192
|
-
var captureTrustedOriginGuard = (req, options, trustedOrigins, action) => {
|
|
1193
|
-
const guarded = action === "login" || action === "exchange";
|
|
1484
|
+
var captureTrustedOriginGuard = (req, options, trustedOrigins, action, strict = false) => {
|
|
1485
|
+
const guarded = strict || action === "login" || action === "exchange";
|
|
1194
1486
|
if (!guarded && !usesCookieTransport(options)) {
|
|
1195
1487
|
return () => {
|
|
1196
1488
|
};
|
|
@@ -1958,6 +2250,28 @@ var createExchangeResponse = (options, session, issuedToken) => ({
|
|
|
1958
2250
|
expiresIn: issuedToken.expiresIn,
|
|
1959
2251
|
tokenType: issuedToken.tokenType
|
|
1960
2252
|
});
|
|
2253
|
+
var notifyTokenOutcomes = (operation, session, issuedToken, notify, diagnostic) => {
|
|
2254
|
+
const deviceBound = session.deviceBinding !== void 0;
|
|
2255
|
+
const correlation = diagnosticCorrelation(
|
|
2256
|
+
deviceBound && !diagnostic?.hasAuthenticatedProof() ? "candidate-session" : "authenticated",
|
|
2257
|
+
session.subject,
|
|
2258
|
+
session.logicalSessionId ?? session.sessionId
|
|
2259
|
+
);
|
|
2260
|
+
const tokenIssued = issuedToken.accessToken !== void 0;
|
|
2261
|
+
if (tokenIssued)
|
|
2262
|
+
notify(
|
|
2263
|
+
Object.freeze({
|
|
2264
|
+
kind: "token-issued",
|
|
2265
|
+
operation,
|
|
2266
|
+
correlation,
|
|
2267
|
+
deviceBound,
|
|
2268
|
+
expiresIn: issuedToken.expiresIn,
|
|
2269
|
+
...issuedToken.tokenType === void 0 ? {} : { tokenType: issuedToken.tokenType }
|
|
2270
|
+
})
|
|
2271
|
+
);
|
|
2272
|
+
if (operation === "exchange")
|
|
2273
|
+
notify(Object.freeze({ kind: "exchange-succeeded", operation, correlation, deviceBound, tokenIssued }));
|
|
2274
|
+
};
|
|
1961
2275
|
var assertIssuedBoundJwt = (accessToken, jkt) => {
|
|
1962
2276
|
if (!isCompactSignedJwt(accessToken)) {
|
|
1963
2277
|
throw new TypeError("tokenIssuer.issue accessToken must be a compact signed JWT for bound issuance.");
|
|
@@ -1984,13 +2298,14 @@ var assertIssuedBoundJwt = (accessToken, jkt) => {
|
|
|
1984
2298
|
throw new TypeError("tokenIssuer.issue accessToken exp must be a nonnegative safe integer with safe milliseconds.");
|
|
1985
2299
|
}
|
|
1986
2300
|
};
|
|
1987
|
-
var withIssuedToken = async (req, res, options, session, verifiedBinding) => {
|
|
2301
|
+
var withIssuedToken = async (req, res, options, session, operation, verifiedBinding) => {
|
|
1988
2302
|
const binding = resolveVerifiedDeviceBinding(options.deviceBinding, session.deviceBinding, verifiedBinding);
|
|
1989
2303
|
if (!options.tokenIssuer) return {};
|
|
1990
2304
|
const logicalSessionId = session.logicalSessionId ?? session.sessionId;
|
|
1991
2305
|
try {
|
|
1992
2306
|
const issuerSession = withSessionDeviceBinding(copyVaultSession(session, true), binding);
|
|
1993
2307
|
const result = await options.tokenIssuer.issue({
|
|
2308
|
+
operation,
|
|
1994
2309
|
req,
|
|
1995
2310
|
res,
|
|
1996
2311
|
session: issuerSession,
|
|
@@ -2029,7 +2344,32 @@ var withIssuedToken = async (req, res, options, session, verifiedBinding) => {
|
|
|
2029
2344
|
};
|
|
2030
2345
|
|
|
2031
2346
|
// src/token-validation.ts
|
|
2032
|
-
import { jwtVerify } from "jose";
|
|
2347
|
+
import { errors as joseErrors, jwtVerify } from "jose";
|
|
2348
|
+
|
|
2349
|
+
// src/access-token-errors.ts
|
|
2350
|
+
var OidcVaultInvalidAccessTokenError = class extends OidcVaultHttpError {
|
|
2351
|
+
constructor(message = "Access token rejected.") {
|
|
2352
|
+
super(401, "OIDC_VAULT_INVALID_ACCESS_TOKEN", message, "Access token validation failed.");
|
|
2353
|
+
this.name = "OidcVaultInvalidAccessTokenError";
|
|
2354
|
+
}
|
|
2355
|
+
};
|
|
2356
|
+
var OidcVaultAuthDeniedError = class extends OidcVaultHttpError {
|
|
2357
|
+
constructor(message = "Authentication denied.", status = 403) {
|
|
2358
|
+
if (status !== 401 && status !== 403) throw new TypeError("Auth denial status must be 401 or 403.");
|
|
2359
|
+
super(status, "OIDC_VAULT_AUTH_DENIED", message);
|
|
2360
|
+
this.name = "OidcVaultAuthDeniedError";
|
|
2361
|
+
}
|
|
2362
|
+
};
|
|
2363
|
+
var credentialOutcomes = /* @__PURE__ */ new WeakMap();
|
|
2364
|
+
var markAccessTokenRejected = (error) => {
|
|
2365
|
+
if (typeof error === "object" && error !== null) credentialOutcomes.set(error, true);
|
|
2366
|
+
};
|
|
2367
|
+
var markAccessTokenValidationFailure = (error) => {
|
|
2368
|
+
if (typeof error === "object" && error !== null) credentialOutcomes.set(error, false);
|
|
2369
|
+
};
|
|
2370
|
+
var isAccessTokenRejected = (error) => typeof error === "object" && error !== null && (credentialOutcomes.get(error) ?? error instanceof OidcVaultInvalidAccessTokenError);
|
|
2371
|
+
|
|
2372
|
+
// src/token-validation.ts
|
|
2033
2373
|
var BACKCHANNEL_LOGOUT_EVENT_CLAIM = "http://schemas.openid.net/event/backchannel-logout";
|
|
2034
2374
|
var validateBackchannelLogoutTokenTyp = (typ) => {
|
|
2035
2375
|
if (typ !== void 0 && (typeof typ !== "string" || typ.toLowerCase() !== "logout+jwt")) {
|
|
@@ -2188,20 +2528,36 @@ function createOidcVaultJwtAccessTokenValidator(options) {
|
|
|
2188
2528
|
const audience = Array.isArray(configuredAudience) ? [...configuredAudience] : configuredAudience;
|
|
2189
2529
|
const algorithms = configuredAlgorithms === void 0 ? void 0 : [...configuredAlgorithms];
|
|
2190
2530
|
const validateJwt = async (token) => {
|
|
2191
|
-
const result = await jwtVerify(token, key, { issuer, audience, algorithms })
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
if (confirmation !== null) {
|
|
2195
|
-
if (!isCompactSignedJwt(token)) {
|
|
2196
|
-
throw invalidAccessToken("Bound JWT must use canonical compact signed encoding.");
|
|
2531
|
+
const result = await jwtVerify(token, key, { issuer, audience, algorithms }).catch((error) => {
|
|
2532
|
+
if (error instanceof joseErrors.JWTExpired || error instanceof joseErrors.JWTClaimValidationFailed || error instanceof joseErrors.JWTInvalid || error instanceof joseErrors.JWSInvalid || error instanceof joseErrors.JWSSignatureVerificationFailed || error instanceof joseErrors.JOSEAlgNotAllowed) {
|
|
2533
|
+
markAccessTokenRejected(error);
|
|
2197
2534
|
}
|
|
2198
|
-
|
|
2199
|
-
|
|
2535
|
+
throw error;
|
|
2536
|
+
});
|
|
2537
|
+
const claims = result.payload;
|
|
2538
|
+
let confirmation;
|
|
2539
|
+
try {
|
|
2540
|
+
confirmation = snapshotVerifiedJwtConfirmation(claims);
|
|
2541
|
+
if (confirmation !== null) {
|
|
2542
|
+
if (!isCompactSignedJwt(token)) {
|
|
2543
|
+
throw invalidAccessToken("Bound JWT must use canonical compact signed encoding.");
|
|
2544
|
+
}
|
|
2545
|
+
if (readJwtExpirationMs(claims.exp) === void 0) {
|
|
2546
|
+
throw invalidAccessToken("Bound JWT exp must be a nonnegative safe integer with safe milliseconds.");
|
|
2547
|
+
}
|
|
2200
2548
|
}
|
|
2549
|
+
if (!mapClaims) return { ...defaultJwtClaimsMapper(claims), confirmation };
|
|
2550
|
+
} catch (error) {
|
|
2551
|
+
markAccessTokenRejected(error);
|
|
2552
|
+
throw error;
|
|
2553
|
+
}
|
|
2554
|
+
try {
|
|
2555
|
+
const { subject, sessionId, scope, claims: mappedClaims } = mapClaims(claims);
|
|
2556
|
+
return { subject, sessionId, scope, claims: mappedClaims, confirmation };
|
|
2557
|
+
} catch (error) {
|
|
2558
|
+
markAccessTokenValidationFailure(error);
|
|
2559
|
+
throw error;
|
|
2201
2560
|
}
|
|
2202
|
-
const mapped = mapClaims ? mapClaims(claims) : defaultJwtClaimsMapper(claims);
|
|
2203
|
-
const { subject, sessionId, scope, claims: mappedClaims } = mapped;
|
|
2204
|
-
return { subject, sessionId, scope, claims: mappedClaims, confirmation };
|
|
2205
2561
|
};
|
|
2206
2562
|
return Object.freeze({
|
|
2207
2563
|
async validate(token) {
|
|
@@ -2216,22 +2572,8 @@ function createOidcVaultJwtAccessTokenValidator(options) {
|
|
|
2216
2572
|
});
|
|
2217
2573
|
}
|
|
2218
2574
|
|
|
2219
|
-
// src/types.ts
|
|
2220
|
-
var OidcVaultStoreConflictError = class extends Error {
|
|
2221
|
-
constructor(message = "OIDC vault store operation conflicted with concurrent state changes.") {
|
|
2222
|
-
super(message);
|
|
2223
|
-
this.name = "OidcVaultStoreConflictError";
|
|
2224
|
-
}
|
|
2225
|
-
};
|
|
2226
|
-
var OidcVaultDpopReplayCapacityError = class extends Error {
|
|
2227
|
-
constructor(message = "OIDC vault DPoP replay store capacity is exhausted.") {
|
|
2228
|
-
super(message);
|
|
2229
|
-
this.name = "OidcVaultDpopReplayCapacityError";
|
|
2230
|
-
}
|
|
2231
|
-
};
|
|
2232
|
-
|
|
2233
2575
|
// src/dpop-replay.ts
|
|
2234
|
-
import { createHash as
|
|
2576
|
+
import { createHash as createHash7 } from "crypto";
|
|
2235
2577
|
var isPrintableIdentifier = (value) => typeof value === "string" && /^[\x20-\x7e]{1,128}$/.test(value);
|
|
2236
2578
|
var normalizeNamespaceBasePath = (origin, value) => {
|
|
2237
2579
|
if (value !== void 0 && typeof value !== "string")
|
|
@@ -2269,7 +2611,7 @@ var createDpopReplayKey = (effectiveNamespace, jkt, jti) => {
|
|
|
2269
2611
|
if (!isCanonicalDpopJkt(jkt) || !isPrintableIdentifier(jti)) {
|
|
2270
2612
|
throw new TypeError("DPoP replay key requires a canonical thumbprint and a bounded printable JTI.");
|
|
2271
2613
|
}
|
|
2272
|
-
return `dpop:v1:${
|
|
2614
|
+
return `dpop:v1:${createHash7("sha256").update(JSON.stringify([effectiveNamespace, jkt, jti]), "utf8").digest("base64url")}`;
|
|
2273
2615
|
};
|
|
2274
2616
|
var getDpopReplayExpiresAt = (input) => {
|
|
2275
2617
|
const { iat, now, proofMaxAgeSeconds: age, clockSkewSeconds: skew } = input;
|
|
@@ -2308,47 +2650,85 @@ var createDpopReplayPolicy = (options) => {
|
|
|
2308
2650
|
});
|
|
2309
2651
|
return Object.freeze({
|
|
2310
2652
|
effectiveNamespace,
|
|
2311
|
-
async verifyAndReserve(verifyBeforeReplay) {
|
|
2653
|
+
async verifyAndReserve(verifyBeforeReplay, diagnostic) {
|
|
2312
2654
|
const verified = await verifyBeforeReplay();
|
|
2313
2655
|
if (typeof verified !== "object" || verified === null || Array.isArray(verified)) {
|
|
2656
|
+
diagnostic?.emit("claims", "rejected", "invalid-claims");
|
|
2314
2657
|
throw invalidProof2("DPoP verifier did not return a proof verification result.");
|
|
2315
2658
|
}
|
|
2316
2659
|
const { binding: verifiedBinding, iat, jti, nonce } = verified;
|
|
2317
2660
|
if (typeof verifiedBinding !== "object" || verifiedBinding === null || Array.isArray(verifiedBinding)) {
|
|
2661
|
+
diagnostic?.emit("binding", "rejected", "invalid-key");
|
|
2318
2662
|
throw invalidProof2("DPoP verifier did not return a verified key binding.");
|
|
2319
2663
|
}
|
|
2320
2664
|
const { type, jkt, alg } = verifiedBinding;
|
|
2321
2665
|
if (type !== "dpop" || !isCanonicalDpopJkt(jkt) || !isDpopAlgorithm(alg) || !policy.algorithms.includes(alg) || !isPrintableIdentifier(jti) || typeof iat !== "number" || !Number.isSafeInteger(iat) || iat < 0) {
|
|
2666
|
+
diagnostic?.emit("claims", "rejected", "invalid-claims");
|
|
2322
2667
|
throw invalidProof2("DPoP verified key/algorithm or signed iat/JTI shape is invalid.");
|
|
2323
2668
|
}
|
|
2324
2669
|
const binding = Object.freeze({ type, jkt, alg });
|
|
2325
2670
|
let checkedNow;
|
|
2671
|
+
let clockSource = configuredNow ? "configured" : "system";
|
|
2326
2672
|
try {
|
|
2327
2673
|
checkedNow = now();
|
|
2328
2674
|
if (!Number.isSafeInteger(checkedNow) || checkedNow < 0) throw new TypeError("DPoP replay clock is invalid.");
|
|
2329
2675
|
} catch (error) {
|
|
2676
|
+
diagnostic?.mark({ freshness: policy.ignoreFreshnessFailure ? "ignored" : "failed" });
|
|
2677
|
+
diagnostic?.emit("freshness", policy.ignoreFreshnessFailure ? "ignored" : "rejected", "clock-unavailable");
|
|
2330
2678
|
handleDpopCheckFailure(policy.ignoreFreshnessFailure, "freshness", replayUnavailable(error));
|
|
2331
2679
|
checkedNow = Date.now();
|
|
2332
|
-
if (!Number.isSafeInteger(checkedNow) || checkedNow < 0)
|
|
2680
|
+
if (!Number.isSafeInteger(checkedNow) || checkedNow < 0) {
|
|
2681
|
+
diagnostic?.mark({ freshness: "failed" });
|
|
2682
|
+
diagnostic?.emit("freshness", "rejected", "clock-unavailable");
|
|
2333
2683
|
throw replayUnavailable(new TypeError("DPoP fallback clock is invalid."));
|
|
2684
|
+
}
|
|
2685
|
+
clockSource = "fallback";
|
|
2334
2686
|
}
|
|
2687
|
+
diagnostic?.checkTime(checkedNow, clockSource);
|
|
2335
2688
|
let expiresAt = getDpopReplayExpiresAt({ ...policy, iat, now: checkedNow });
|
|
2336
2689
|
if (expiresAt === null) {
|
|
2690
|
+
diagnostic?.mark({ freshness: policy.ignoreFreshnessFailure ? "ignored" : "failed" });
|
|
2691
|
+
diagnostic?.emit(
|
|
2692
|
+
"freshness",
|
|
2693
|
+
policy.ignoreFreshnessFailure ? "ignored" : "rejected",
|
|
2694
|
+
!Number.isSafeInteger(iat * 1e3) || !Number.isSafeInteger((iat + policy.proofMaxAgeSeconds + policy.clockSkewSeconds) * 1e3) ? "invalid-time" : iat * 1e3 - checkedNow > policy.clockSkewSeconds * 1e3 ? "proof-in-future" : "proof-too-old"
|
|
2695
|
+
);
|
|
2337
2696
|
handleDpopCheckFailure(
|
|
2338
2697
|
policy.ignoreFreshnessFailure,
|
|
2339
2698
|
"freshness",
|
|
2340
2699
|
invalidProof2("DPoP signed iat is outside the bounded replay window.")
|
|
2341
2700
|
);
|
|
2342
2701
|
expiresAt = checkedNow + (policy.proofMaxAgeSeconds + policy.clockSkewSeconds) * 1e3;
|
|
2702
|
+
} else {
|
|
2703
|
+
diagnostic?.mark({ freshness: clockSource === "fallback" ? "ignored" : "passed" });
|
|
2704
|
+
diagnostic?.emit("freshness", "accepted", "check-passed");
|
|
2343
2705
|
}
|
|
2344
2706
|
if (noncePolicy) {
|
|
2345
2707
|
if (!noncePolicy.verify(nonce, jkt, checkedNow)) {
|
|
2346
|
-
|
|
2708
|
+
let challengeNonce;
|
|
2709
|
+
try {
|
|
2710
|
+
challengeNonce = noncePolicy.issue(jkt, checkedNow);
|
|
2711
|
+
} catch (error) {
|
|
2712
|
+
diagnostic?.mark({ nonce: "failed" });
|
|
2713
|
+
diagnostic?.emit("nonce", "rejected", "nonce-unavailable");
|
|
2714
|
+
throw error;
|
|
2715
|
+
}
|
|
2716
|
+
diagnostic?.mark({ nonce: "challenged" });
|
|
2717
|
+
diagnostic?.emit("nonce", "challenged", nonce === void 0 ? "nonce-required" : "nonce-invalid");
|
|
2718
|
+
return Object.freeze({ type: "nonce-challenge", nonce: challengeNonce });
|
|
2347
2719
|
}
|
|
2720
|
+
diagnostic?.mark({ nonce: "passed" });
|
|
2721
|
+
diagnostic?.emit("nonce", "accepted", "check-passed");
|
|
2348
2722
|
} else if (nonce !== void 0 && (typeof nonce !== "string" || nonce.length > DPOP_NONCE_MAX_BYTES || Buffer.byteLength(nonce, "utf8") > DPOP_NONCE_MAX_BYTES)) {
|
|
2723
|
+
diagnostic?.mark({ nonce: "failed" });
|
|
2724
|
+
diagnostic?.emit("nonce", "rejected", "invalid-nonce");
|
|
2349
2725
|
throw invalidProof2("DPoP signed nonce claim exceeds its string/byte bound.");
|
|
2726
|
+
} else {
|
|
2727
|
+
diagnostic?.mark({ nonce: "disabled" });
|
|
2728
|
+
diagnostic?.emit("nonce", "ignored", "nonce-disabled");
|
|
2350
2729
|
}
|
|
2351
2730
|
let reservationError;
|
|
2731
|
+
let unavailable = false;
|
|
2352
2732
|
try {
|
|
2353
2733
|
const reserved = await replayStore.reserveDpopProof({
|
|
2354
2734
|
replayKey: createDpopReplayKey(effectiveNamespace, jkt, jti),
|
|
@@ -2359,8 +2739,21 @@ var createDpopReplayPolicy = (options) => {
|
|
|
2359
2739
|
if (!reserved) reservationError = invalidProof2("DPoP replay reservation was duplicate or no longer live.");
|
|
2360
2740
|
} catch (error) {
|
|
2361
2741
|
reservationError = replayUnavailable(error);
|
|
2742
|
+
unavailable = true;
|
|
2362
2743
|
}
|
|
2363
|
-
if (reservationError)
|
|
2744
|
+
if (reservationError) {
|
|
2745
|
+
diagnostic?.mark({ replay: policy.ignoreReplayFailure ? "ignored" : "failed" });
|
|
2746
|
+
diagnostic?.emit(
|
|
2747
|
+
"replay",
|
|
2748
|
+
policy.ignoreReplayFailure ? "ignored" : "rejected",
|
|
2749
|
+
unavailable ? "replay-unavailable" : "replay-detected"
|
|
2750
|
+
);
|
|
2751
|
+
handleDpopCheckFailure(policy.ignoreReplayFailure, "replay", reservationError);
|
|
2752
|
+
} else {
|
|
2753
|
+
diagnostic?.mark({ replay: "reserved" });
|
|
2754
|
+
diagnostic?.emit("replay", "accepted", "check-passed");
|
|
2755
|
+
}
|
|
2756
|
+
diagnostic?.accept();
|
|
2364
2757
|
return Object.freeze({ type: "accepted", binding, expiresAt });
|
|
2365
2758
|
}
|
|
2366
2759
|
});
|
|
@@ -2399,7 +2792,8 @@ var toDpopNonceChallengeResponse = (result, endpoint, algorithms) => {
|
|
|
2399
2792
|
};
|
|
2400
2793
|
|
|
2401
2794
|
// src/vault-route-proof.ts
|
|
2402
|
-
var captureVaultRouteProof = (req) => Object.freeze({
|
|
2795
|
+
var captureVaultRouteProof = (req, diagnostic) => Object.freeze({
|
|
2796
|
+
diagnostic,
|
|
2403
2797
|
header: readDpopRawHeader(req, "dpop"),
|
|
2404
2798
|
method: req.method,
|
|
2405
2799
|
originalUrl: req.originalUrl
|
|
@@ -2423,14 +2817,17 @@ var createVaultRouteProofVerifier = (options, config, backendOrigin, basePath) =
|
|
|
2423
2817
|
now: options.now
|
|
2424
2818
|
});
|
|
2425
2819
|
const admit = async (request, proof, expectedJkt) => {
|
|
2820
|
+
const { diagnostic } = request;
|
|
2426
2821
|
const result = await replay.verifyAndReserve(
|
|
2427
2822
|
() => verifyDpopProof({
|
|
2428
2823
|
proof,
|
|
2429
2824
|
method: request.method,
|
|
2430
2825
|
targetUrl: () => targetFor(request.originalUrl),
|
|
2431
2826
|
proofOptions: policy,
|
|
2432
|
-
expectedJkt
|
|
2433
|
-
|
|
2827
|
+
expectedJkt,
|
|
2828
|
+
diagnostic
|
|
2829
|
+
}),
|
|
2830
|
+
diagnostic
|
|
2434
2831
|
);
|
|
2435
2832
|
if (result.type === "nonce-challenge") {
|
|
2436
2833
|
throw new VaultDpopNonceChallengeError(toDpopNonceChallengeResponse(result, "vault-post", policy.algorithms));
|
|
@@ -2439,9 +2836,10 @@ var createVaultRouteProofVerifier = (options, config, backendOrigin, basePath) =
|
|
|
2439
2836
|
};
|
|
2440
2837
|
return Object.freeze({
|
|
2441
2838
|
async initiate(request) {
|
|
2442
|
-
const proof = extractDpopProof(request.header);
|
|
2839
|
+
const proof = extractDpopProof(request.header, request.diagnostic);
|
|
2443
2840
|
if (proof === void 0) {
|
|
2444
2841
|
assertDeviceBindingPolicy(policy, void 0);
|
|
2842
|
+
request.diagnostic?.emit("presentation", "ignored", "proof-absent");
|
|
2445
2843
|
return void 0;
|
|
2446
2844
|
}
|
|
2447
2845
|
return admit(request, proof);
|
|
@@ -2449,9 +2847,11 @@ var createVaultRouteProofVerifier = (options, config, backendOrigin, basePath) =
|
|
|
2449
2847
|
async verify(request, originalBinding) {
|
|
2450
2848
|
const binding = snapshotDpopBinding(originalBinding);
|
|
2451
2849
|
assertDeviceBindingPolicy(policy, binding);
|
|
2452
|
-
const proof = extractDpopProof(request.header);
|
|
2850
|
+
const proof = extractDpopProof(request.header, request.diagnostic);
|
|
2453
2851
|
const verified = proof === void 0 ? void 0 : await admit(request, proof, binding?.jkt);
|
|
2454
|
-
|
|
2852
|
+
const resolved = resolveVerifiedDeviceBinding(policy, binding, verified);
|
|
2853
|
+
if (proof === void 0) request.diagnostic?.emit("presentation", "ignored", "proof-absent");
|
|
2854
|
+
return resolved;
|
|
2455
2855
|
}
|
|
2456
2856
|
});
|
|
2457
2857
|
};
|
|
@@ -2469,7 +2869,7 @@ var extractBearerToken = (authorizationHeader) => {
|
|
|
2469
2869
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_MISSING_BEARER_TOKEN", "Missing bearer token.");
|
|
2470
2870
|
}
|
|
2471
2871
|
const [scheme, token, extra] = authorizationHeader.trim().split(/\s+/);
|
|
2472
|
-
if (scheme?.toLowerCase() !== "bearer" || !token || extra) {
|
|
2872
|
+
if (scheme?.toLowerCase() !== "bearer" || !token || extra || !/^[\x21-\x7e]+$/.test(token) || token.includes(",")) {
|
|
2473
2873
|
throw new OidcVaultHttpError(
|
|
2474
2874
|
401,
|
|
2475
2875
|
"OIDC_VAULT_INVALID_AUTHORIZATION_HEADER",
|
|
@@ -2496,8 +2896,40 @@ var mappedFields = (result) => {
|
|
|
2496
2896
|
throw invalidAccessToken("Access token validator result is missing subject.");
|
|
2497
2897
|
return { subject, sessionId, scope, claims };
|
|
2498
2898
|
};
|
|
2899
|
+
var evidence = /* @__PURE__ */ new WeakMap();
|
|
2900
|
+
var authFields = ["subject", "sessionId", "scope", "claims", "token", "confirmation", "deviceBinding"];
|
|
2901
|
+
var credentialRejectionCodes = /* @__PURE__ */ new Set([
|
|
2902
|
+
"OIDC_VAULT_MISSING_BEARER_TOKEN",
|
|
2903
|
+
"OIDC_VAULT_MISSING_ACCESS_TOKEN",
|
|
2904
|
+
"OIDC_VAULT_INVALID_AUTHORIZATION_HEADER",
|
|
2905
|
+
"OIDC_VAULT_INVALID_ACCESS_TOKEN",
|
|
2906
|
+
"OIDC_VAULT_DPOP_REQUIRED",
|
|
2907
|
+
"OIDC_VAULT_DEVICE_BINDING_REQUIRED",
|
|
2908
|
+
"OIDC_VAULT_INVALID_DPOP_PROOF"
|
|
2909
|
+
]);
|
|
2910
|
+
var clearChallenge = (res) => {
|
|
2911
|
+
res.removeHeader("WWW-Authenticate");
|
|
2912
|
+
res.removeHeader("DPoP-Nonce");
|
|
2913
|
+
res.setHeader("Cache-Control", "no-store");
|
|
2914
|
+
};
|
|
2915
|
+
var sendError = (res, response) => {
|
|
2916
|
+
clearChallenge(res);
|
|
2917
|
+
for (const [name, value] of Object.entries(response.headers)) res.setHeader(name, value);
|
|
2918
|
+
res.status(response.status).json(response.body);
|
|
2919
|
+
};
|
|
2499
2920
|
function createOidcVaultAccessTokenMiddleware(options) {
|
|
2500
|
-
|
|
2921
|
+
return createOidcVaultAuthenticator(options).authenticate;
|
|
2922
|
+
}
|
|
2923
|
+
function createOidcVaultAuthenticator(options) {
|
|
2924
|
+
const { validator, deviceBinding: configuredBinding, onAuthContext, onError, onDpopDiagnostic } = options;
|
|
2925
|
+
const createDiagnostic = createDpopDiagnosticFactory(onDpopDiagnostic);
|
|
2926
|
+
const { authentication = "required", invalidCredentials = "reject" } = options;
|
|
2927
|
+
if (authentication !== "required" && authentication !== "optional")
|
|
2928
|
+
throw new TypeError("authentication must be required or optional.");
|
|
2929
|
+
if (invalidCredentials !== "reject" && invalidCredentials !== "anonymous")
|
|
2930
|
+
throw new TypeError("invalidCredentials must be reject or anonymous.");
|
|
2931
|
+
if (invalidCredentials === "anonymous" && authentication !== "optional")
|
|
2932
|
+
throw new TypeError("Anonymous invalid credentials require optional authentication.");
|
|
2501
2933
|
const policy = resolveDeviceBindingOptions(configuredBinding);
|
|
2502
2934
|
const validate = validator.validate.bind(validator);
|
|
2503
2935
|
const requestAware = policy ? validator.validateWithRequest : void 0;
|
|
@@ -2529,56 +2961,134 @@ function createOidcVaultAccessTokenMiddleware(options) {
|
|
|
2529
2961
|
const algs = algorithms.join(" ");
|
|
2530
2962
|
const dpopChallenge = (error) => `DPoP${error ? ` error="${error}",` : ""} algs="${algs}"`;
|
|
2531
2963
|
const proofChallenge = dpopChallenge("invalid_dpop_proof");
|
|
2532
|
-
|
|
2964
|
+
const owner = Object.freeze({});
|
|
2965
|
+
const prepareError = (error, scheme, nonceResponse, hookFailure = false) => {
|
|
2966
|
+
const response = nonceResponse ?? (error instanceof OidcVaultHttpError ? toDpopReplayErrorResponse(error, algorithms) : {
|
|
2967
|
+
status: hookFailure ? 500 : 401,
|
|
2968
|
+
body: hookFailure ? { code: "OIDC_VAULT_AUTH_CONTEXT_FAILED", message: "Auth context hook failed." } : { code: "OIDC_VAULT_INVALID_ACCESS_TOKEN", message: "Access token validation failed." },
|
|
2969
|
+
headers: { "Cache-Control": "no-store" }
|
|
2970
|
+
});
|
|
2971
|
+
const headers = { ...response.headers };
|
|
2972
|
+
if (response.status === 401 && headers["WWW-Authenticate"] === void 0) {
|
|
2973
|
+
const code = response.body.code;
|
|
2974
|
+
const challenge2 = code === "OIDC_VAULT_DPOP_REQUIRED" || code === "OIDC_VAULT_INVALID_DPOP_PROOF" ? proofChallenge : !policy ? "Bearer" : code === "OIDC_VAULT_MISSING_ACCESS_TOKEN" || code === "OIDC_VAULT_AUTHENTICATION_REQUIRED" ? policy.mode === "required" ? dpopChallenge() : `Bearer, ${dpopChallenge()}` : scheme === "DPoP" || !scheme && policy.mode === "required" ? dpopChallenge("invalid_token") : 'Bearer error="invalid_token"';
|
|
2975
|
+
headers["WWW-Authenticate"] = challenge2;
|
|
2976
|
+
}
|
|
2977
|
+
return Object.freeze({
|
|
2978
|
+
status: response.status,
|
|
2979
|
+
body: Object.freeze({ ...response.body }),
|
|
2980
|
+
headers: Object.freeze(headers)
|
|
2981
|
+
});
|
|
2982
|
+
};
|
|
2983
|
+
const reentryError = () => new OidcVaultHttpError(500, "OIDC_VAULT_AUTHENTICATION_REENTRY", "Authentication is already running or completed.");
|
|
2984
|
+
const rejectGuard = async (req, res, next) => {
|
|
2985
|
+
const state = evidence.get(req);
|
|
2986
|
+
const error = state?.status === "pending" ? reentryError() : new OidcVaultHttpError(401, "OIDC_VAULT_AUTHENTICATION_REQUIRED", "Authentication is required.");
|
|
2987
|
+
if (state) state.status = "failed";
|
|
2988
|
+
else evidence.set(req, { owner, res, status: "failed" });
|
|
2989
|
+
delete req.auth;
|
|
2990
|
+
const response = prepareError(error);
|
|
2991
|
+
await notifyMiddlewareError(onError, { error, req, res });
|
|
2992
|
+
delete req.auth;
|
|
2993
|
+
if (res.headersSent) next(error);
|
|
2994
|
+
else sendError(res, response);
|
|
2995
|
+
};
|
|
2996
|
+
const guard = (bound) => async (req, res, next) => {
|
|
2997
|
+
const state = evidence.get(req);
|
|
2998
|
+
const current = Object.getOwnPropertyDescriptor(req, "auth");
|
|
2999
|
+
const intact = state?.auth && state.snapshot && current && "value" in current && current.value === state.auth && authFields.every((key) => {
|
|
3000
|
+
const field = Object.getOwnPropertyDescriptor(state.auth, key);
|
|
3001
|
+
return Object.hasOwn(state.snapshot, key) ? field && "value" in field && field.value === state.snapshot[key] : field === void 0;
|
|
3002
|
+
});
|
|
3003
|
+
if (state?.status !== "authenticated" || state.owner !== owner || state.res !== res || !intact) {
|
|
3004
|
+
await rejectGuard(req, res, next);
|
|
3005
|
+
return;
|
|
3006
|
+
}
|
|
3007
|
+
if (bound && !state.snapshot.deviceBinding) {
|
|
3008
|
+
const error = new OidcVaultHttpError(
|
|
3009
|
+
401,
|
|
3010
|
+
"OIDC_VAULT_DEVICE_BINDING_REQUIRED",
|
|
3011
|
+
"A device-bound login is required."
|
|
3012
|
+
);
|
|
3013
|
+
state.status = "failed";
|
|
3014
|
+
delete req.auth;
|
|
3015
|
+
const response = prepareError(error);
|
|
3016
|
+
await notifyMiddlewareError(onError, { error, req, res });
|
|
3017
|
+
delete req.auth;
|
|
3018
|
+
if (res.headersSent) next(error);
|
|
3019
|
+
else sendError(res, response);
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
3022
|
+
if (!res.headersSent) res.setHeader("Cache-Control", "no-store");
|
|
3023
|
+
next();
|
|
3024
|
+
};
|
|
3025
|
+
const authenticate = async (req, res, next) => {
|
|
3026
|
+
if (evidence.has(req)) {
|
|
3027
|
+
const state2 = evidence.get(req);
|
|
3028
|
+
state2.status = "failed";
|
|
3029
|
+
delete req.auth;
|
|
3030
|
+
const error = reentryError();
|
|
3031
|
+
const response = prepareError(error);
|
|
3032
|
+
await notifyMiddlewareError(onError, { error, req, res });
|
|
3033
|
+
delete req.auth;
|
|
3034
|
+
if (res.headersSent) next(error);
|
|
3035
|
+
else sendError(res, response);
|
|
3036
|
+
return;
|
|
3037
|
+
}
|
|
3038
|
+
const state = { owner, res, status: "pending" };
|
|
3039
|
+
const diagnostic = createDiagnostic("api");
|
|
3040
|
+
evidence.set(req, state);
|
|
2533
3041
|
let token;
|
|
2534
3042
|
let scheme;
|
|
2535
3043
|
let nonceResponse;
|
|
3044
|
+
let stage = "credential";
|
|
2536
3045
|
const authenticatedRequest = req;
|
|
2537
3046
|
delete authenticatedRequest.auth;
|
|
2538
3047
|
if (!res.headersSent) res.setHeader("Cache-Control", "no-store");
|
|
2539
|
-
const
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
headers: { "Cache-Control": "no-store" }
|
|
2544
|
-
});
|
|
2545
|
-
const headers = { ...response.headers };
|
|
2546
|
-
if (response.status === 401 && headers["WWW-Authenticate"] === void 0) {
|
|
2547
|
-
const code = response.body.code;
|
|
2548
|
-
const challenge2 = code === "OIDC_VAULT_DPOP_REQUIRED" || code === "OIDC_VAULT_INVALID_DPOP_PROOF" ? proofChallenge : !policy ? "Bearer" : code === "OIDC_VAULT_MISSING_ACCESS_TOKEN" ? policy.mode === "required" ? dpopChallenge() : `Bearer, ${dpopChallenge()}` : scheme === "DPoP" || !scheme && policy.mode === "required" ? dpopChallenge("invalid_token") : 'Bearer error="invalid_token"';
|
|
2549
|
-
headers["WWW-Authenticate"] = challenge2;
|
|
2550
|
-
}
|
|
2551
|
-
return Object.freeze({
|
|
2552
|
-
status: response.status,
|
|
2553
|
-
body: Object.freeze({ ...response.body }),
|
|
2554
|
-
headers: Object.freeze(headers)
|
|
2555
|
-
});
|
|
2556
|
-
};
|
|
2557
|
-
const sendError = (response) => {
|
|
2558
|
-
res.removeHeader("WWW-Authenticate");
|
|
2559
|
-
res.removeHeader("DPoP-Nonce");
|
|
2560
|
-
for (const [name, value] of Object.entries(response.headers)) res.setHeader(name, value);
|
|
2561
|
-
res.status(response.status).json(response.body);
|
|
3048
|
+
const stillPending = () => {
|
|
3049
|
+
if (state.status === "pending") return true;
|
|
3050
|
+
delete authenticatedRequest.auth;
|
|
3051
|
+
return false;
|
|
2562
3052
|
};
|
|
2563
3053
|
try {
|
|
3054
|
+
const authorization = readDpopRawHeader(req, "authorization");
|
|
3055
|
+
const proofHeader = readDpopRawHeader(req, "dpop");
|
|
3056
|
+
if (authentication === "optional" && authorization.count === 0 && proofHeader.count === 0) {
|
|
3057
|
+
diagnostic.emit("presentation", "ignored", policy ? "proof-absent" : "policy-disabled");
|
|
3058
|
+
state.status = "missing";
|
|
3059
|
+
clearChallenge(res);
|
|
3060
|
+
next();
|
|
3061
|
+
return;
|
|
3062
|
+
}
|
|
2564
3063
|
let result;
|
|
2565
3064
|
let confirmation;
|
|
2566
3065
|
let deviceBinding;
|
|
2567
3066
|
if (!policy) {
|
|
3067
|
+
if (authorization.count > 1 || authorization.count === 1 && !authorization.value?.trim())
|
|
3068
|
+
throw new OidcVaultHttpError(
|
|
3069
|
+
401,
|
|
3070
|
+
"OIDC_VAULT_INVALID_AUTHORIZATION_HEADER",
|
|
3071
|
+
"Authorization header must use the Bearer scheme."
|
|
3072
|
+
);
|
|
2568
3073
|
token = extractBearerToken(req.get("authorization"));
|
|
2569
3074
|
scheme = "Bearer";
|
|
3075
|
+
stage = "validator";
|
|
2570
3076
|
result = await validate(token);
|
|
3077
|
+
if (!stillPending()) return;
|
|
2571
3078
|
delete authenticatedRequest.auth;
|
|
3079
|
+
stage = "result";
|
|
2572
3080
|
const reported = result?.confirmation;
|
|
2573
3081
|
confirmation = reported === void 0 ? void 0 : snapshotAccessTokenConfirmation(reported);
|
|
2574
3082
|
result = mappedFields(result);
|
|
3083
|
+
diagnostic.associate(diagnosticCorrelation("verified-token", result.subject, result.sessionId));
|
|
2575
3084
|
delete authenticatedRequest.auth;
|
|
3085
|
+
stage = "proof";
|
|
2576
3086
|
if (confirmation)
|
|
2577
3087
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_DPOP_REQUIRED", "DPoP authentication is required.");
|
|
3088
|
+
diagnostic.emit("presentation", "ignored", "policy-disabled");
|
|
2578
3089
|
} else {
|
|
2579
|
-
const authorization = readDpopRawHeader(req, "authorization");
|
|
2580
3090
|
scheme = schemeOf(authorization.value);
|
|
2581
|
-
if (authorization.count === 0
|
|
3091
|
+
if (authorization.count === 0) {
|
|
2582
3092
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_MISSING_ACCESS_TOKEN", "Missing access token.");
|
|
2583
3093
|
}
|
|
2584
3094
|
const credential = authorization.value?.trim().split(/\s+/);
|
|
@@ -2590,13 +3100,17 @@ function createOidcVaultAccessTokenMiddleware(options) {
|
|
|
2590
3100
|
);
|
|
2591
3101
|
}
|
|
2592
3102
|
token = credential[1];
|
|
2593
|
-
const proofHeader = readDpopRawHeader(req, "dpop");
|
|
2594
3103
|
const { method, originalUrl } = req;
|
|
3104
|
+
stage = "validator";
|
|
2595
3105
|
result = await validateWithRequest(Object.freeze({ token, scheme, req }));
|
|
3106
|
+
if (!stillPending()) return;
|
|
2596
3107
|
delete authenticatedRequest.auth;
|
|
3108
|
+
stage = "result";
|
|
2597
3109
|
confirmation = snapshotAccessTokenConfirmation(result?.confirmation);
|
|
2598
3110
|
result = mappedFields(result);
|
|
3111
|
+
diagnostic.associate(diagnosticCorrelation("verified-token", result.subject, result.sessionId));
|
|
2599
3112
|
delete authenticatedRequest.auth;
|
|
3113
|
+
stage = "proof";
|
|
2600
3114
|
if (confirmation && scheme !== "DPoP")
|
|
2601
3115
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_DPOP_REQUIRED", "DPoP authentication is required.");
|
|
2602
3116
|
if (!confirmation && scheme === "DPoP")
|
|
@@ -2604,7 +3118,7 @@ function createOidcVaultAccessTokenMiddleware(options) {
|
|
|
2604
3118
|
if (!confirmation && policy.mode === "required") {
|
|
2605
3119
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_DEVICE_BINDING_REQUIRED", "A device-bound login is required.");
|
|
2606
3120
|
}
|
|
2607
|
-
const proof = extractDpopProof(proofHeader);
|
|
3121
|
+
const proof = extractDpopProof(proofHeader, diagnostic);
|
|
2608
3122
|
if (!proof && confirmation)
|
|
2609
3123
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_DPOP_REQUIRED", "DPoP authentication is required.");
|
|
2610
3124
|
if (proof) {
|
|
@@ -2615,15 +3129,18 @@ function createOidcVaultAccessTokenMiddleware(options) {
|
|
|
2615
3129
|
targetUrl: () => targetFor(originalUrl),
|
|
2616
3130
|
proofOptions: policy,
|
|
2617
3131
|
accessToken: token,
|
|
2618
|
-
expectedJkt: confirmation?.jkt
|
|
2619
|
-
|
|
3132
|
+
expectedJkt: confirmation?.jkt,
|
|
3133
|
+
diagnostic
|
|
3134
|
+
}),
|
|
3135
|
+
diagnostic
|
|
2620
3136
|
);
|
|
3137
|
+
if (!stillPending()) return;
|
|
2621
3138
|
if (admission.type === "nonce-challenge") {
|
|
2622
3139
|
nonceResponse = toDpopNonceChallengeResponse(admission, "api", policy.algorithms);
|
|
2623
3140
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_USE_DPOP_NONCE", "A fresh DPoP nonce is required.");
|
|
2624
3141
|
}
|
|
2625
3142
|
if (confirmation) deviceBinding = admission.binding;
|
|
2626
|
-
}
|
|
3143
|
+
} else diagnostic.emit("presentation", "ignored", "proof-absent");
|
|
2627
3144
|
}
|
|
2628
3145
|
const authenticatedToken = token;
|
|
2629
3146
|
const auth = {
|
|
@@ -2647,9 +3164,12 @@ function createOidcVaultAccessTokenMiddleware(options) {
|
|
|
2647
3164
|
authenticatedRequest.auth = auth;
|
|
2648
3165
|
try {
|
|
2649
3166
|
await onAuthContext?.({ req: authenticatedRequest, res, auth });
|
|
3167
|
+
if (!stillPending()) return;
|
|
2650
3168
|
authenticatedRequest.auth = restoreAuth();
|
|
2651
3169
|
} catch (hookError) {
|
|
2652
|
-
|
|
3170
|
+
if (!stillPending()) return;
|
|
3171
|
+
state.status = "failed";
|
|
3172
|
+
const response = prepareError(hookError, scheme, void 0, true);
|
|
2653
3173
|
delete authenticatedRequest.auth;
|
|
2654
3174
|
let errorAuth;
|
|
2655
3175
|
try {
|
|
@@ -2668,13 +3188,25 @@ function createOidcVaultAccessTokenMiddleware(options) {
|
|
|
2668
3188
|
next(hookError);
|
|
2669
3189
|
return;
|
|
2670
3190
|
}
|
|
2671
|
-
sendError(response);
|
|
3191
|
+
sendError(res, response);
|
|
2672
3192
|
return;
|
|
2673
3193
|
}
|
|
3194
|
+
state.auth = authenticatedRequest.auth;
|
|
3195
|
+
state.snapshot = { ...authenticatedRequest.auth };
|
|
3196
|
+
state.status = "authenticated";
|
|
2674
3197
|
if (!res.headersSent) res.setHeader("Cache-Control", "no-store");
|
|
2675
3198
|
next();
|
|
2676
3199
|
} catch (error) {
|
|
2677
|
-
|
|
3200
|
+
if (!stillPending()) return;
|
|
3201
|
+
diagnostic.emit(
|
|
3202
|
+
"preflight",
|
|
3203
|
+
"rejected",
|
|
3204
|
+
stage === "proof" ? diagnosticPreflightReason(error instanceof OidcVaultHttpError ? error.code : void 0) : "credential-rejected"
|
|
3205
|
+
);
|
|
3206
|
+
const response = prepareError(error, scheme, nonceResponse);
|
|
3207
|
+
const rejected = stage === "validator" ? isAccessTokenRejected(error) : (stage === "credential" || stage === "proof") && error instanceof OidcVaultHttpError && credentialRejectionCodes.has(error.code);
|
|
3208
|
+
const anonymous = authentication === "optional" && invalidCredentials === "anonymous" && rejected && !nonceResponse;
|
|
3209
|
+
state.status = anonymous ? "invalid" : nonceResponse ? "challenged" : "failed";
|
|
2678
3210
|
delete authenticatedRequest.auth;
|
|
2679
3211
|
await notifyMiddlewareError(onError, { error, req, res, ...token ? { token } : {} });
|
|
2680
3212
|
delete authenticatedRequest.auth;
|
|
@@ -2682,9 +3214,139 @@ function createOidcVaultAccessTokenMiddleware(options) {
|
|
|
2682
3214
|
next(error);
|
|
2683
3215
|
return;
|
|
2684
3216
|
}
|
|
2685
|
-
|
|
3217
|
+
if (anonymous && state.status === "invalid") {
|
|
3218
|
+
clearChallenge(res);
|
|
3219
|
+
res.status(200);
|
|
3220
|
+
next();
|
|
3221
|
+
} else sendError(res, response);
|
|
2686
3222
|
}
|
|
2687
3223
|
};
|
|
3224
|
+
return Object.freeze({ authenticate, requireAuth: guard(false), requireDeviceBinding: guard(true) });
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
// src/jwt-token-issuer.ts
|
|
3228
|
+
import { createPrivateKey, createSecretKey, KeyObject } from "crypto";
|
|
3229
|
+
import { types } from "util";
|
|
3230
|
+
import { SignJWT } from "jose";
|
|
3231
|
+
var ALGORITHMS = /* @__PURE__ */ new Set([
|
|
3232
|
+
"HS256",
|
|
3233
|
+
"HS384",
|
|
3234
|
+
"HS512",
|
|
3235
|
+
"RS256",
|
|
3236
|
+
"RS384",
|
|
3237
|
+
"RS512",
|
|
3238
|
+
"PS256",
|
|
3239
|
+
"PS384",
|
|
3240
|
+
"PS512",
|
|
3241
|
+
"ES256",
|
|
3242
|
+
"ES384",
|
|
3243
|
+
"ES512",
|
|
3244
|
+
"EdDSA"
|
|
3245
|
+
]);
|
|
3246
|
+
var RESERVED = ["iss", "aud", "iat", "exp", "sub", "cnf"];
|
|
3247
|
+
var CURVES = { ES256: "prime256v1", ES384: "secp384r1", ES512: "secp521r1" };
|
|
3248
|
+
var requireString = (value, name) => {
|
|
3249
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new TypeError(`${name} must be a nonblank string.`);
|
|
3250
|
+
return value;
|
|
3251
|
+
};
|
|
3252
|
+
var signingKey = (configured, algorithm) => {
|
|
3253
|
+
const symmetric = algorithm.startsWith("HS");
|
|
3254
|
+
let key;
|
|
3255
|
+
let cryptoKey;
|
|
3256
|
+
if (configured instanceof Uint8Array) {
|
|
3257
|
+
key = createSecretKey(Uint8Array.from(configured));
|
|
3258
|
+
} else if (configured instanceof KeyObject) {
|
|
3259
|
+
key = configured;
|
|
3260
|
+
} else if (types.isCryptoKey(configured)) {
|
|
3261
|
+
cryptoKey = configured;
|
|
3262
|
+
const expectedName = symmetric ? "HMAC" : algorithm.startsWith("RS") ? "RSASSA-PKCS1-v1_5" : algorithm.startsWith("PS") ? "RSA-PSS" : algorithm.startsWith("ES") ? "ECDSA" : "Ed25519";
|
|
3263
|
+
const parameters = configured.algorithm;
|
|
3264
|
+
if (!configured.usages.includes("sign") || parameters.name !== expectedName || (symmetric || algorithm.startsWith("RS") || algorithm.startsWith("PS")) && parameters.hash?.name !== `SHA-${algorithm.slice(2)}`)
|
|
3265
|
+
throw new TypeError("JWT signing CryptoKey algorithm/hash/usages must match the signing algorithm.");
|
|
3266
|
+
key = KeyObject.from(configured);
|
|
3267
|
+
} else {
|
|
3268
|
+
if (!isRecord(configured)) throw new TypeError("JWT signing key must be a JOSE key.");
|
|
3269
|
+
const jwk = { ...configured, ...configured.key_ops ? { key_ops: [...configured.key_ops] } : {} };
|
|
3270
|
+
if (jwk.alg !== void 0 && jwk.alg !== algorithm || jwk.use !== void 0 && jwk.use !== "sig" || jwk.key_ops !== void 0 && (!Array.isArray(jwk.key_ops) || !jwk.key_ops.includes("sign")))
|
|
3271
|
+
throw new TypeError("JWT signing JWK alg/use/key_ops must permit the signing algorithm.");
|
|
3272
|
+
if (jwk.kty === "oct") {
|
|
3273
|
+
if (typeof jwk.k !== "string" || !/^[A-Za-z0-9_-]+$/.test(jwk.k) || Buffer.from(jwk.k, "base64url").toString("base64url") !== jwk.k)
|
|
3274
|
+
throw new TypeError("JWT signing JWK k must be canonical base64url secret bytes.");
|
|
3275
|
+
key = createSecretKey(Buffer.from(jwk.k, "base64url"));
|
|
3276
|
+
} else {
|
|
3277
|
+
key = createPrivateKey({ key: jwk, format: "jwk" });
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
const bits = Number(algorithm.slice(2));
|
|
3281
|
+
const curve = CURVES[algorithm];
|
|
3282
|
+
const compatible = symmetric ? key.type === "secret" && key.symmetricKeySize * 8 >= bits : key.type === "private" && (curve ? key.asymmetricKeyType === "ec" && key.asymmetricKeyDetails?.namedCurve === curve : algorithm === "EdDSA" ? key.asymmetricKeyType === "ed25519" : key.asymmetricKeyType === "rsa" && (key.asymmetricKeyDetails?.modulusLength ?? 0) >= 2048);
|
|
3283
|
+
if (!compatible) throw new TypeError("JWT signing key type/size/curve must match the signing algorithm.");
|
|
3284
|
+
return cryptoKey ?? key;
|
|
3285
|
+
};
|
|
3286
|
+
var tokenTimes = (now, lifetimeSeconds) => {
|
|
3287
|
+
const milliseconds = now();
|
|
3288
|
+
const lifetimeMs = lifetimeSeconds * 1e3;
|
|
3289
|
+
const iat = Math.floor(milliseconds / 1e3);
|
|
3290
|
+
const exp = iat + lifetimeSeconds;
|
|
3291
|
+
if (!Number.isSafeInteger(milliseconds) || milliseconds < 0 || !Number.isSafeInteger(lifetimeMs) || !Number.isSafeInteger(milliseconds + lifetimeMs) || !Number.isSafeInteger(exp) || !Number.isSafeInteger(exp * 1e3))
|
|
3292
|
+
throw new TypeError("JWT clock/lifetime must produce safe nonnegative integer milliseconds and expiry.");
|
|
3293
|
+
return { iat, exp };
|
|
3294
|
+
};
|
|
3295
|
+
function assertApplicationClaims(claims) {
|
|
3296
|
+
if (!isRecord(claims) || Array.isArray(claims)) throw new TypeError("JWT mapClaims must return a claims object.");
|
|
3297
|
+
for (const field of RESERVED) {
|
|
3298
|
+
if (field in claims) throw new TypeError(`JWT mapClaims must not supply reserved claim ${field}.`);
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
function createOidcVaultJwtAccessTokenIssuer(options) {
|
|
3302
|
+
const {
|
|
3303
|
+
algorithm,
|
|
3304
|
+
issuer: configuredIssuer,
|
|
3305
|
+
audience: configuredAudience,
|
|
3306
|
+
lifetimeSeconds = 300,
|
|
3307
|
+
now = Date.now,
|
|
3308
|
+
mapSubject,
|
|
3309
|
+
mapClaims
|
|
3310
|
+
} = options;
|
|
3311
|
+
if (!ALGORITHMS.has(algorithm)) throw new TypeError("JWT signing algorithm is unsupported.");
|
|
3312
|
+
const issuer = requireString(configuredIssuer, "JWT issuer");
|
|
3313
|
+
const audience = Array.isArray(configuredAudience) ? [...configuredAudience] : configuredAudience;
|
|
3314
|
+
if (Array.isArray(audience)) {
|
|
3315
|
+
if (audience.length === 0) throw new TypeError("JWT audience must not be empty.");
|
|
3316
|
+
for (const value of audience) requireString(value, "JWT audience");
|
|
3317
|
+
} else requireString(audience, "JWT audience");
|
|
3318
|
+
if (!Number.isSafeInteger(lifetimeSeconds) || lifetimeSeconds < 0)
|
|
3319
|
+
throw new TypeError("JWT lifetimeSeconds must be a nonnegative safe integer.");
|
|
3320
|
+
if (typeof now !== "function" || mapSubject !== void 0 && typeof mapSubject !== "function" || mapClaims !== void 0 && typeof mapClaims !== "function")
|
|
3321
|
+
throw new TypeError("JWT clock and mappers must be functions.");
|
|
3322
|
+
const key = signingKey(options.key, algorithm);
|
|
3323
|
+
tokenTimes(now, lifetimeSeconds);
|
|
3324
|
+
return Object.freeze({
|
|
3325
|
+
async issue(input) {
|
|
3326
|
+
const { deviceBinding, session } = input;
|
|
3327
|
+
const { subject: defaultSubject, scope } = session;
|
|
3328
|
+
if (scope !== void 0 && typeof scope !== "string") throw new TypeError("JWT session scope must be a string.");
|
|
3329
|
+
if (deviceBinding !== void 0 && deviceBinding.type !== "dpop")
|
|
3330
|
+
throw new TypeError("JWT deviceBinding must be verified DPoP context.");
|
|
3331
|
+
const cnf = deviceBinding === void 0 ? void 0 : snapshotJkt({ jkt: deviceBinding.jkt }, (message) => new TypeError(message));
|
|
3332
|
+
const subject = requireString(mapSubject ? await mapSubject(input) : defaultSubject, "JWT subject");
|
|
3333
|
+
const mapped = mapClaims ? await mapClaims(input) : {};
|
|
3334
|
+
assertApplicationClaims(mapped);
|
|
3335
|
+
const claims = JSON.parse(JSON.stringify({ ...scope === void 0 ? {} : { scope }, ...mapped }));
|
|
3336
|
+
assertApplicationClaims(claims);
|
|
3337
|
+
const { iat, exp } = tokenTimes(now, lifetimeSeconds);
|
|
3338
|
+
const accessToken = await new SignJWT({
|
|
3339
|
+
...claims,
|
|
3340
|
+
iss: issuer,
|
|
3341
|
+
aud: audience,
|
|
3342
|
+
sub: subject,
|
|
3343
|
+
iat,
|
|
3344
|
+
exp,
|
|
3345
|
+
...cnf === void 0 ? {} : { cnf }
|
|
3346
|
+
}).setProtectedHeader({ alg: algorithm, typ: "JWT" }).sign(key);
|
|
3347
|
+
return { accessToken, expiresIn: lifetimeSeconds, tokenType: cnf === void 0 ? "Bearer" : "DPoP" };
|
|
3348
|
+
}
|
|
3349
|
+
});
|
|
2688
3350
|
}
|
|
2689
3351
|
|
|
2690
3352
|
// src/index.ts
|
|
@@ -2699,24 +3361,30 @@ var CREDENTIAL_RESPONSE_CACHE_CONTROL = "no-store";
|
|
|
2699
3361
|
var applyCredentialResponseCachePolicy = (res) => {
|
|
2700
3362
|
res.setHeader("Cache-Control", CREDENTIAL_RESPONSE_CACHE_CONTROL);
|
|
2701
3363
|
};
|
|
2702
|
-
var createOpaqueId = (prefix) => `${prefix}_${
|
|
2703
|
-
var createPkceVerifier = () =>
|
|
2704
|
-
var createPkceChallenge = (verifier) =>
|
|
3364
|
+
var createOpaqueId = (prefix) => `${prefix}_${randomBytes4(16).toString("base64url")}`;
|
|
3365
|
+
var createPkceVerifier = () => randomBytes4(32).toString("base64url");
|
|
3366
|
+
var createPkceChallenge = (verifier) => createHash8("sha256").update(verifier).digest("base64url");
|
|
2705
3367
|
var getCallbackUri = (backendOrigin, basePath) => `${backendOrigin}${basePath}${OIDC_VAULT_ROUTE_PATHS.callback}`;
|
|
2706
3368
|
var validateOidcVaultOptions = (options) => {
|
|
2707
3369
|
const {
|
|
2708
3370
|
deviceBinding: configuredDeviceBinding,
|
|
2709
3371
|
fingerprintRecognition: configuredFingerprintRecognition,
|
|
2710
3372
|
transactionCookie: configuredTransactionCookie,
|
|
3373
|
+
logoutNavigation: configuredLogoutNavigation,
|
|
2711
3374
|
...otherOptions
|
|
2712
3375
|
} = options;
|
|
2713
3376
|
const cookie = otherOptions.cookie ? { ...otherOptions.cookie } : void 0;
|
|
2714
3377
|
const cookieOptions = { ...otherOptions, cookie };
|
|
2715
|
-
|
|
3378
|
+
const now = getNow(options);
|
|
3379
|
+
validateLifetimeOptions(options, now);
|
|
2716
3380
|
const deviceBinding = resolveDeviceBindingOptions(configuredDeviceBinding);
|
|
2717
3381
|
const fingerprintRecognition = resolveFingerprintRecognitionOptions(configuredFingerprintRecognition);
|
|
2718
3382
|
const backendOrigin = deviceBinding === void 0 ? resolveBackendOrigin(options) : resolveDeviceBindingOrigin(options.backendOrigin);
|
|
2719
|
-
if (deviceBinding !== void 0)
|
|
3383
|
+
if (deviceBinding !== void 0)
|
|
3384
|
+
assertDeviceBindingStoreCapabilities(
|
|
3385
|
+
options.storeProvider,
|
|
3386
|
+
configuredLogoutNavigation === void 0 ? "deviceBinding" : "logoutNavigation"
|
|
3387
|
+
);
|
|
2720
3388
|
if (fingerprintRecognition !== void 0)
|
|
2721
3389
|
assertDeviceBindingStoreCapabilities(options.storeProvider, "fingerprintRecognition");
|
|
2722
3390
|
const frontendRedirectUri = resolveFrontendRedirectUri(options);
|
|
@@ -2732,6 +3400,16 @@ var validateOidcVaultOptions = (options) => {
|
|
|
2732
3400
|
backendOrigin
|
|
2733
3401
|
);
|
|
2734
3402
|
const config = resolveOidcVaultConfig(options.config ? { ...options.config } : void 0);
|
|
3403
|
+
const logoutNavigation = resolveLogoutNavigationOptions(configuredLogoutNavigation, {
|
|
3404
|
+
backendOrigin,
|
|
3405
|
+
basePath: normalizeOidcVaultBasePath(otherOptions.basePath),
|
|
3406
|
+
config,
|
|
3407
|
+
frontendRedirectUri,
|
|
3408
|
+
postLogoutRedirectUri: otherOptions.postLogoutRedirectUri,
|
|
3409
|
+
deviceBindingConfigured: deviceBinding !== void 0,
|
|
3410
|
+
store: otherOptions.storeProvider,
|
|
3411
|
+
now
|
|
3412
|
+
});
|
|
2735
3413
|
const configuredTrustedOrigins = resolveTrustedOrigins(options);
|
|
2736
3414
|
if (usesCrossSiteCookieTransport(cookieOptions) && configuredTrustedOrigins.size === 0) {
|
|
2737
3415
|
throw new Error("trustedOrigins is required when using cross-site cookie transport.");
|
|
@@ -2743,6 +3421,8 @@ var validateOidcVaultOptions = (options) => {
|
|
|
2743
3421
|
deviceBinding,
|
|
2744
3422
|
fingerprintRecognition,
|
|
2745
3423
|
transactionCookie,
|
|
3424
|
+
logoutNavigation,
|
|
3425
|
+
...logoutNavigation === void 0 ? {} : { postLogoutRedirectUri: logoutNavigation.postLogoutRedirectUri },
|
|
2746
3426
|
...frontendRedirectUri !== void 0 ? { frontendRedirectUri } : {},
|
|
2747
3427
|
...options.trustedOrigins ? { trustedOrigins: [...options.trustedOrigins] } : {},
|
|
2748
3428
|
...cookie === void 0 ? {} : { cookie },
|
|
@@ -2890,10 +3570,18 @@ async function handleRouteError(route, req, res, next, options, error) {
|
|
|
2890
3570
|
sendVaultRoutePolicyResponse(res, response);
|
|
2891
3571
|
}
|
|
2892
3572
|
function createAsyncHandler(route, options, handler) {
|
|
3573
|
+
const createDiagnostic = createDpopDiagnosticFactory(options.onDpopDiagnostic);
|
|
3574
|
+
const notify = createDiagnosticDispatcher(options.onTokenOutcome);
|
|
2893
3575
|
return async (req, res, next) => {
|
|
3576
|
+
const diagnostic = req.method === "POST" && route !== "callback" && route !== "backchannel-logout" ? createDiagnostic(route) : void 0;
|
|
2894
3577
|
try {
|
|
2895
|
-
await handler(req, res, next);
|
|
3578
|
+
await handler(req, res, next, diagnostic, notify);
|
|
2896
3579
|
} catch (error) {
|
|
3580
|
+
diagnostic?.emit(
|
|
3581
|
+
"preflight",
|
|
3582
|
+
"rejected",
|
|
3583
|
+
diagnosticPreflightReason(error instanceof OidcVaultHttpError ? error.code : void 0)
|
|
3584
|
+
);
|
|
2897
3585
|
await handleRouteError(route, req, res, next, options, error);
|
|
2898
3586
|
}
|
|
2899
3587
|
};
|
|
@@ -2939,8 +3627,8 @@ var createLoginHandler = (options, config, backendOrigin, basePath) => createAsy
|
|
|
2939
3627
|
});
|
|
2940
3628
|
res.redirect(302, authorizationUrl);
|
|
2941
3629
|
});
|
|
2942
|
-
var createLoginInitiationHandler = (options, config, trustedOrigins, backendOrigin, basePath, proofVerifier) => createAsyncHandler("login", options, async (req, res) => {
|
|
2943
|
-
const capturedProof = captureVaultRouteProof(req);
|
|
3630
|
+
var createLoginInitiationHandler = (options, config, trustedOrigins, backendOrigin, basePath, proofVerifier) => createAsyncHandler("login", options, async (req, res, _next, diagnostic) => {
|
|
3631
|
+
const capturedProof = captureVaultRouteProof(req, diagnostic);
|
|
2944
3632
|
const fingerprintRecognition = captureFingerprintRecognition(req, options.fingerprintRecognition);
|
|
2945
3633
|
assertLoginInitiationRequest(req, options, trustedOrigins);
|
|
2946
3634
|
const body = getBody(req);
|
|
@@ -2949,6 +3637,7 @@ var createLoginInitiationHandler = (options, config, trustedOrigins, backendOrig
|
|
|
2949
3637
|
}
|
|
2950
3638
|
const returnTo = resolveLoginReturnTo(body.returnTo, options);
|
|
2951
3639
|
if (proofVerifier === void 0 && capturedProof.header.count !== 0) {
|
|
3640
|
+
diagnostic?.emit("presentation", "rejected", "policy-disabled");
|
|
2952
3641
|
throw new OidcVaultHttpError(
|
|
2953
3642
|
401,
|
|
2954
3643
|
"OIDC_VAULT_INVALID_DPOP_PROOF",
|
|
@@ -2957,6 +3646,7 @@ var createLoginInitiationHandler = (options, config, trustedOrigins, backendOrig
|
|
|
2957
3646
|
);
|
|
2958
3647
|
}
|
|
2959
3648
|
const verifiedBinding = await proofVerifier?.initiate(capturedProof);
|
|
3649
|
+
if (proofVerifier === void 0) diagnostic?.emit("presentation", "ignored", "policy-disabled");
|
|
2960
3650
|
const binding = snapshotDpopBinding(verifiedBinding);
|
|
2961
3651
|
const metadata = await resolveProviderMetadata(config, options);
|
|
2962
3652
|
await callHook("login", options.hooks?.onLoginStart, req, res, void 0, { provider: { ...metadata } });
|
|
@@ -3174,8 +3864,8 @@ var copySessionApplicationFields = (session) => {
|
|
|
3174
3864
|
}
|
|
3175
3865
|
return result;
|
|
3176
3866
|
};
|
|
3177
|
-
var createExchangeHandler = (options, config, trustedOrigins, proofVerifier) => createAsyncHandler("exchange", options, async (req, res) => {
|
|
3178
|
-
const capturedProof = proofVerifier === void 0 ? void 0 : captureVaultRouteProof(req);
|
|
3867
|
+
var createExchangeHandler = (options, config, trustedOrigins, proofVerifier) => createAsyncHandler("exchange", options, async (req, res, _next, diagnostic, notify) => {
|
|
3868
|
+
const capturedProof = proofVerifier === void 0 ? void 0 : captureVaultRouteProof(req, diagnostic);
|
|
3179
3869
|
const capturedRecognition = captureFingerprintRecognition(req, options.fingerprintRecognition);
|
|
3180
3870
|
const assertExchangeOrigin = captureTrustedOriginGuard(req, options, trustedOrigins, "exchange");
|
|
3181
3871
|
const cookieHeader = req.headers.cookie;
|
|
@@ -3187,14 +3877,24 @@ var createExchangeHandler = (options, config, trustedOrigins, proofVerifier) =>
|
|
|
3187
3877
|
throw new TypeError("Device-bound exchange requires guarded store reads and atomic consumption.");
|
|
3188
3878
|
}
|
|
3189
3879
|
const rawCode = canPreflight ? await store.getExchangeCode(code) : await store.consumeExchangeCode(code);
|
|
3190
|
-
if (!rawCode)
|
|
3880
|
+
if (!rawCode) {
|
|
3881
|
+
diagnostic?.emit("preflight", "rejected", "session-unavailable");
|
|
3882
|
+
throw invalidExchangeCode();
|
|
3883
|
+
}
|
|
3191
3884
|
const record = snapshotVaultExchangeCode(rawCode, code, getNow(options));
|
|
3885
|
+
diagnostic?.associate(diagnosticCorrelation("candidate-session", void 0, record.sessionId));
|
|
3192
3886
|
const guarded = record.browserBindingHash !== void 0;
|
|
3193
3887
|
if (guarded) assertExchangeOrigin();
|
|
3194
3888
|
assertDeviceBindingPolicy(options.deviceBinding, record.deviceBinding);
|
|
3195
3889
|
const rawSession = await store.getSession(record.sessionId);
|
|
3196
|
-
if (!rawSession)
|
|
3890
|
+
if (!rawSession) {
|
|
3891
|
+
diagnostic?.emit("preflight", "rejected", "session-unavailable");
|
|
3892
|
+
throw invalidVaultSession();
|
|
3893
|
+
}
|
|
3197
3894
|
const session = snapshotVaultSession(rawSession, record.sessionId, getNow(options));
|
|
3895
|
+
diagnostic?.associate(
|
|
3896
|
+
diagnosticCorrelation("candidate-session", session.subject, session.logicalSessionId ?? session.sessionId)
|
|
3897
|
+
);
|
|
3198
3898
|
assertDeviceBindingPolicy(options.deviceBinding, session.deviceBinding);
|
|
3199
3899
|
assertVaultExchangeSessionBinding(record, session);
|
|
3200
3900
|
const protectedExchange = proofVerifier !== void 0 || options.fingerprintRecognition !== void 0 || guarded;
|
|
@@ -3206,6 +3906,7 @@ var createExchangeHandler = (options, config, trustedOrigins, proofVerifier) =>
|
|
|
3206
3906
|
const recognition = options.fingerprintRecognition === void 0 ? void 0 : snapshotFingerprintRecognition(session.metadata);
|
|
3207
3907
|
assertFingerprintRecognition(options.fingerprintRecognition, recognition, capturedRecognition);
|
|
3208
3908
|
const verifiedBinding = proofVerifier === void 0 ? void 0 : await proofVerifier.verify(capturedProof, session.deviceBinding);
|
|
3909
|
+
if (proofVerifier === void 0) diagnostic?.emit("presentation", "ignored", "policy-disabled");
|
|
3209
3910
|
if (canPreflight) {
|
|
3210
3911
|
const consumed = protectedExchange ? await store.consumeExchangeCodeIfMatches({
|
|
3211
3912
|
code,
|
|
@@ -3226,34 +3927,45 @@ var createExchangeHandler = (options, config, trustedOrigins, proofVerifier) =>
|
|
|
3226
3927
|
}
|
|
3227
3928
|
assertVaultSessionIdentity(session, config);
|
|
3228
3929
|
try {
|
|
3229
|
-
const issuedToken = await withIssuedToken(req, res, options, session, verifiedBinding);
|
|
3930
|
+
const issuedToken = await withIssuedToken(req, res, options, session, "exchange", verifiedBinding);
|
|
3230
3931
|
if (usesCookieTransport(options)) setSessionCookie(res, options, session.sessionId);
|
|
3231
3932
|
if (cookieSecret !== void 0) clearTransactionCookie(res, options.transactionCookie);
|
|
3232
3933
|
applyCredentialResponseCachePolicy(res);
|
|
3233
|
-
|
|
3934
|
+
const response = createExchangeResponse(options, session, issuedToken);
|
|
3935
|
+
notifyTokenOutcomes("exchange", session, issuedToken, notify, diagnostic);
|
|
3936
|
+
res.status(200).json(response);
|
|
3234
3937
|
} catch (error) {
|
|
3235
3938
|
if (cookieSecret !== void 0) clearTransactionCookie(res, options.transactionCookie);
|
|
3236
3939
|
throw error;
|
|
3237
3940
|
}
|
|
3238
3941
|
});
|
|
3239
|
-
var createRefreshHandler = (options, config, trustedOrigins, proofVerifier) => createAsyncHandler("refresh", options, async (req, res) => {
|
|
3942
|
+
var createRefreshHandler = (options, config, trustedOrigins, proofVerifier) => createAsyncHandler("refresh", options, async (req, res, _next, diagnostic, notify) => {
|
|
3240
3943
|
const capturedRecognition = captureFingerprintRecognition(req, options.fingerprintRecognition);
|
|
3241
3944
|
assertTrustedOrigin(req, options, trustedOrigins, "refresh");
|
|
3242
|
-
const capturedProof = proofVerifier === void 0 ? void 0 : captureVaultRouteProof(req);
|
|
3945
|
+
const capturedProof = proofVerifier === void 0 ? void 0 : captureVaultRouteProof(req, diagnostic);
|
|
3243
3946
|
const sessionId = getSessionIdFromRequest(req, options, "refresh");
|
|
3244
3947
|
const currentRecord = await options.storeProvider.getSession(sessionId);
|
|
3245
3948
|
if (!currentRecord) {
|
|
3949
|
+
diagnostic?.emit("preflight", "rejected", "session-unavailable");
|
|
3246
3950
|
if (usesCookieTransport(options)) {
|
|
3247
3951
|
clearSessionCookie(res, options);
|
|
3248
3952
|
}
|
|
3249
3953
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_SESSION", "Session is missing or expired.");
|
|
3250
3954
|
}
|
|
3251
3955
|
const currentSession = snapshotVaultSession(currentRecord, sessionId, getNow(options));
|
|
3956
|
+
diagnostic?.associate(
|
|
3957
|
+
diagnosticCorrelation(
|
|
3958
|
+
"candidate-session",
|
|
3959
|
+
currentSession.subject,
|
|
3960
|
+
currentSession.logicalSessionId ?? currentSession.sessionId
|
|
3961
|
+
)
|
|
3962
|
+
);
|
|
3252
3963
|
assertVaultSessionIdentity(currentSession, config);
|
|
3253
3964
|
assertDeviceBindingPolicy(options.deviceBinding, currentSession.deviceBinding);
|
|
3254
3965
|
const recognition = options.fingerprintRecognition === void 0 ? void 0 : snapshotFingerprintRecognition(currentSession.metadata);
|
|
3255
3966
|
assertFingerprintRecognition(options.fingerprintRecognition, recognition, capturedRecognition);
|
|
3256
3967
|
const verifiedBinding = proofVerifier === void 0 ? void 0 : await proofVerifier.verify(capturedProof, currentSession.deviceBinding);
|
|
3968
|
+
if (proofVerifier === void 0) diagnostic?.emit("presentation", "ignored", "policy-disabled");
|
|
3257
3969
|
const metadata = await resolveProviderMetadata(config, options);
|
|
3258
3970
|
if (proofVerifier !== void 0 || options.fingerprintRecognition !== void 0) {
|
|
3259
3971
|
const latest = await options.storeProvider.getSession(sessionId);
|
|
@@ -3327,7 +4039,7 @@ var createRefreshHandler = (options, config, trustedOrigins, proofVerifier) => c
|
|
|
3327
4039
|
throw error;
|
|
3328
4040
|
}
|
|
3329
4041
|
const rotatedSession = expectedSession;
|
|
3330
|
-
const issuedToken = await withIssuedToken(req, res, options, rotatedSession, verifiedBinding);
|
|
4042
|
+
const issuedToken = await withIssuedToken(req, res, options, rotatedSession, "refresh", verifiedBinding);
|
|
3331
4043
|
await callPostCommitHook(
|
|
3332
4044
|
"refresh",
|
|
3333
4045
|
options,
|
|
@@ -3344,14 +4056,21 @@ var createRefreshHandler = (options, config, trustedOrigins, proofVerifier) => c
|
|
|
3344
4056
|
}
|
|
3345
4057
|
const response = createExchangeResponse(options, rotatedSession, issuedToken);
|
|
3346
4058
|
applyCredentialResponseCachePolicy(res);
|
|
4059
|
+
notifyTokenOutcomes("refresh", rotatedSession, issuedToken, notify, diagnostic);
|
|
3347
4060
|
res.status(200).json(response);
|
|
3348
4061
|
});
|
|
3349
|
-
var
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
4062
|
+
var notifyLogoutError = async (options, req, res, error, session) => {
|
|
4063
|
+
try {
|
|
4064
|
+
await options.hooks?.onError?.({
|
|
4065
|
+
...createHookContext("logout", req, res, session === void 0 ? void 0 : copyVaultSession(session)),
|
|
4066
|
+
error
|
|
4067
|
+
});
|
|
4068
|
+
} catch {
|
|
4069
|
+
}
|
|
4070
|
+
};
|
|
4071
|
+
var performLogout = async (options, config, proofVerifier, req, res, diagnostic, redirect, navigation) => {
|
|
4072
|
+
const capturedProof = proofVerifier === void 0 ? void 0 : captureVaultRouteProof(req, diagnostic);
|
|
3353
4073
|
const sessionId = getSessionIdFromRequest(req, options, "logout");
|
|
3354
|
-
const redirect = body.redirect === true;
|
|
3355
4074
|
const store = options.storeProvider;
|
|
3356
4075
|
const hasContext = typeof store.getSessionRevocationContext === "function";
|
|
3357
4076
|
if (proofVerifier !== void 0 && !hasContext) {
|
|
@@ -3361,18 +4080,29 @@ var createLogoutHandler = (options, config, trustedOrigins, proofVerifier) => cr
|
|
|
3361
4080
|
const context = rawContext ? snapshotVaultRevocationContext(rawContext) : void 0;
|
|
3362
4081
|
const rawSession = await store.getSession(sessionId);
|
|
3363
4082
|
const session = rawSession ? snapshotVaultSession(rawSession, sessionId, getNow(options)) : void 0;
|
|
4083
|
+
if (session || context)
|
|
4084
|
+
diagnostic?.associate(
|
|
4085
|
+
diagnosticCorrelation(
|
|
4086
|
+
"candidate-session",
|
|
4087
|
+
session?.subject,
|
|
4088
|
+
context?.logicalSessionId ?? session?.logicalSessionId ?? session?.sessionId
|
|
4089
|
+
)
|
|
4090
|
+
);
|
|
3364
4091
|
const finishLocalLogout = () => {
|
|
4092
|
+
diagnostic?.emit(
|
|
4093
|
+
"presentation",
|
|
4094
|
+
"ignored",
|
|
4095
|
+
options.deviceBinding === void 0 ? "policy-disabled" : "target-absent"
|
|
4096
|
+
);
|
|
3365
4097
|
if (usesCookieTransport(options)) clearSessionCookie(res, options);
|
|
3366
|
-
|
|
3367
|
-
res.status(200).json({ loggedOut: true });
|
|
4098
|
+
return {};
|
|
3368
4099
|
};
|
|
3369
4100
|
if (hasContext && context === void 0) {
|
|
3370
4101
|
if (session !== void 0) {
|
|
3371
4102
|
assertDeviceBindingPolicy(options.deviceBinding, session.deviceBinding);
|
|
3372
4103
|
throw invalidVaultSession();
|
|
3373
4104
|
}
|
|
3374
|
-
finishLocalLogout();
|
|
3375
|
-
return;
|
|
4105
|
+
return finishLocalLogout();
|
|
3376
4106
|
}
|
|
3377
4107
|
const authority = context ?? (session === void 0 ? void 0 : snapshotVaultRevocationContext({
|
|
3378
4108
|
logicalSessionId: session.logicalSessionId ?? session.sessionId,
|
|
@@ -3381,11 +4111,12 @@ var createLogoutHandler = (options, config, trustedOrigins, proofVerifier) => cr
|
|
|
3381
4111
|
}));
|
|
3382
4112
|
if (authority === void 0) {
|
|
3383
4113
|
await store.deleteSession(sessionId);
|
|
3384
|
-
finishLocalLogout();
|
|
3385
|
-
return;
|
|
4114
|
+
return finishLocalLogout();
|
|
3386
4115
|
}
|
|
3387
4116
|
assertVaultSessionIdentity(authority, config);
|
|
3388
4117
|
assertDeviceBindingPolicy(options.deviceBinding, authority.deviceBinding);
|
|
4118
|
+
if (navigation !== void 0 && authority.deviceBinding === void 0)
|
|
4119
|
+
throw new OidcVaultHttpError(401, "OIDC_VAULT_DEVICE_BINDING_REQUIRED", "A device-bound login is required.");
|
|
3389
4120
|
if (session !== void 0)
|
|
3390
4121
|
assertVaultRevocationContextMatches(
|
|
3391
4122
|
snapshotVaultRevocationContext({
|
|
@@ -3396,54 +4127,95 @@ var createLogoutHandler = (options, config, trustedOrigins, proofVerifier) => cr
|
|
|
3396
4127
|
authority
|
|
3397
4128
|
);
|
|
3398
4129
|
if (proofVerifier !== void 0) await proofVerifier.verify(capturedProof, authority.deviceBinding);
|
|
4130
|
+
else diagnostic?.emit("presentation", "ignored", "policy-disabled");
|
|
3399
4131
|
if (hasContext) {
|
|
3400
4132
|
const latest = await store.getSessionRevocationContext(sessionId);
|
|
3401
4133
|
if (!latest) {
|
|
3402
|
-
finishLocalLogout();
|
|
3403
|
-
return;
|
|
4134
|
+
return finishLocalLogout();
|
|
3404
4135
|
}
|
|
3405
4136
|
assertVaultRevocationContextMatches(snapshotVaultRevocationContext(latest), authority);
|
|
3406
4137
|
}
|
|
3407
4138
|
if (session !== void 0)
|
|
3408
4139
|
await callHook("logout", options.hooks?.onBeforeLogout, req, res, copyVaultSession(session));
|
|
3409
4140
|
await store.deleteSessionsByLogicalSessionId({ logicalSessionId: authority.logicalSessionId });
|
|
3410
|
-
|
|
3411
|
-
clearSessionCookie(res, options);
|
|
4141
|
+
try {
|
|
4142
|
+
if (usesCookieTransport(options)) clearSessionCookie(res, options);
|
|
4143
|
+
if (session === void 0 || !redirect && navigation === void 0) return {};
|
|
4144
|
+
if (navigation !== void 0 && !isString(session.idToken)) return {};
|
|
4145
|
+
try {
|
|
4146
|
+
const metadata = await resolveProviderMetadata(config, options);
|
|
4147
|
+
if (!metadata.endSessionEndpoint) return {};
|
|
4148
|
+
const endpoint = navigation === void 0 ? metadata.endSessionEndpoint : validateUpstreamLogoutUrl(metadata.endSessionEndpoint);
|
|
4149
|
+
const builtUrl = buildLogoutUrl(endpoint, session.idToken, options.postLogoutRedirectUri);
|
|
4150
|
+
if (navigation === void 0) return { upstreamLogoutUrl: builtUrl };
|
|
4151
|
+
const upstreamLogoutUrl = validateUpstreamLogoutUrl(builtUrl);
|
|
4152
|
+
const navigationUrl = await persistLogoutNavigation(navigation, store, upstreamLogoutUrl, getNow(options));
|
|
4153
|
+
return { navigationUrl };
|
|
4154
|
+
} catch (error) {
|
|
4155
|
+
await notifyLogoutError(options, req, res, error, session);
|
|
4156
|
+
return {};
|
|
4157
|
+
}
|
|
4158
|
+
} finally {
|
|
4159
|
+
if (session !== void 0)
|
|
4160
|
+
await callPostCommitHook("logout", options, options.hooks?.onLogout, req, res, copyVaultSession(session));
|
|
3412
4161
|
}
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
4162
|
+
};
|
|
4163
|
+
var createLogoutHandler = (options, config, trustedOrigins, proofVerifier, navigation) => createAsyncHandler("logout", options, async (req, res, _next, diagnostic) => {
|
|
4164
|
+
if (navigation === void 0) assertTrustedOrigin(req, options, trustedOrigins, "logout");
|
|
4165
|
+
else {
|
|
4166
|
+
assertLogoutNavigationRequest(req, options, trustedOrigins);
|
|
4167
|
+
const body = req.body;
|
|
4168
|
+
if (typeof body !== "object" || body === null || Array.isArray(body) || Object.keys(body).some((key) => key !== "sessionId") || Object.hasOwn(body, "sessionId") && typeof body.sessionId !== "string")
|
|
4169
|
+
throw new OidcVaultHttpError(
|
|
4170
|
+
400,
|
|
4171
|
+
"OIDC_VAULT_INVALID_REQUEST_BODY",
|
|
4172
|
+
"Logout navigation request body is invalid."
|
|
4173
|
+
);
|
|
4174
|
+
}
|
|
4175
|
+
const redirect = navigation === void 0 && getBody(req).redirect === true;
|
|
4176
|
+
const result = await performLogout(options, config, proofVerifier, req, res, diagnostic, redirect, navigation);
|
|
4177
|
+
applyCredentialResponseCachePolicy(res);
|
|
4178
|
+
if (navigation !== void 0) {
|
|
4179
|
+
res.status(200).json({
|
|
4180
|
+
loggedOut: true,
|
|
4181
|
+
navigationUrl: result.navigationUrl ?? navigation.navigationUrl
|
|
4182
|
+
});
|
|
3416
4183
|
return;
|
|
3417
4184
|
}
|
|
3418
|
-
if (
|
|
3419
|
-
|
|
3420
|
-
applyCredentialResponseCachePolicy(res);
|
|
3421
|
-
res.status(200).json({ loggedOut: true });
|
|
4185
|
+
if (result.upstreamLogoutUrl) {
|
|
4186
|
+
res.redirect(302, result.upstreamLogoutUrl);
|
|
3422
4187
|
return;
|
|
3423
4188
|
}
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
4189
|
+
res.status(200).json({ loggedOut: true });
|
|
4190
|
+
});
|
|
4191
|
+
var assertLogoutNavigationRequest = (req, options, trustedOrigins) => {
|
|
4192
|
+
captureTrustedOriginGuard(req, options, trustedOrigins, "logout", true)();
|
|
4193
|
+
if (!req.is("application/json"))
|
|
4194
|
+
throw new OidcVaultHttpError(
|
|
4195
|
+
415,
|
|
4196
|
+
"OIDC_VAULT_UNSUPPORTED_REQUEST_BODY_TYPE",
|
|
4197
|
+
"Logout navigation requires a JSON request body."
|
|
4198
|
+
);
|
|
4199
|
+
};
|
|
4200
|
+
var createLogoutNavigationGetHandler = (options) => async (req, res) => {
|
|
4201
|
+
const policy = options.logoutNavigation;
|
|
4202
|
+
let location = policy.fallbackRedirectUri;
|
|
4203
|
+
const ticketHash = navigationTicketHash(req.originalUrl);
|
|
4204
|
+
if (ticketHash !== void 0) {
|
|
3429
4205
|
try {
|
|
3430
|
-
await options.
|
|
3431
|
-
|
|
3432
|
-
|
|
4206
|
+
const record = await options.storeProvider.consumeLogoutNavigationTicket({
|
|
4207
|
+
namespace: policy.namespace,
|
|
4208
|
+
ticketHash
|
|
3433
4209
|
});
|
|
3434
|
-
|
|
4210
|
+
if (record !== null)
|
|
4211
|
+
location = consumedLogoutDestination(record, policy.namespace, ticketHash, getNow(options));
|
|
4212
|
+
} catch (error) {
|
|
4213
|
+
await notifyLogoutError(options, req, res, error);
|
|
3435
4214
|
}
|
|
3436
4215
|
}
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
res.redirect(302, upstreamLogoutUrl);
|
|
3441
|
-
return;
|
|
3442
|
-
}
|
|
3443
|
-
res.status(200).json({
|
|
3444
|
-
loggedOut: true
|
|
3445
|
-
});
|
|
3446
|
-
});
|
|
4216
|
+
res.status(302).setHeader("Location", location);
|
|
4217
|
+
res.end();
|
|
4218
|
+
};
|
|
3447
4219
|
var createBackchannelLogoutHandler = (options, config) => createAsyncHandler("backchannel-logout", options, async (req, res) => {
|
|
3448
4220
|
const metadata = await resolveProviderMetadata(config, options);
|
|
3449
4221
|
const logoutToken = getLogoutTokenFromRequest(req);
|
|
@@ -3505,6 +4277,10 @@ function registerRoutes(router, options, config, trustedOrigins, backendOrigin,
|
|
|
3505
4277
|
router.post(OIDC_VAULT_ROUTE_PATHS.exchange, createExchangeHandler(options, config, trustedOrigins, proofVerifier));
|
|
3506
4278
|
router.post(OIDC_VAULT_ROUTE_PATHS.refresh, createRefreshHandler(options, config, trustedOrigins, proofVerifier));
|
|
3507
4279
|
router.post(OIDC_VAULT_ROUTE_PATHS.logout, createLogoutHandler(options, config, trustedOrigins, proofVerifier));
|
|
4280
|
+
if (options.logoutNavigation !== void 0) {
|
|
4281
|
+
const path = OIDC_VAULT_ROUTE_PATHS["logout-navigation"];
|
|
4282
|
+
router.post(path, createLogoutHandler(options, config, trustedOrigins, proofVerifier, options.logoutNavigation));
|
|
4283
|
+
}
|
|
3508
4284
|
router.post(OIDC_VAULT_ROUTE_PATHS["backchannel-logout"], createBackchannelLogoutHandler(options, config));
|
|
3509
4285
|
}
|
|
3510
4286
|
function createOidcVaultMiddleware(options) {
|
|
@@ -3517,6 +4293,26 @@ function createOidcVaultMiddleware(options) {
|
|
|
3517
4293
|
applyCredentialResponseCachePolicy(res);
|
|
3518
4294
|
next();
|
|
3519
4295
|
});
|
|
4296
|
+
if (resolvedOptions.logoutNavigation !== void 0) {
|
|
4297
|
+
const path = OIDC_VAULT_ROUTE_PATHS["logout-navigation"];
|
|
4298
|
+
baseRouter.all(path, (_req, res, next) => {
|
|
4299
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
4300
|
+
next();
|
|
4301
|
+
});
|
|
4302
|
+
baseRouter.head(path, (_req, res) => {
|
|
4303
|
+
res.setHeader("Allow", "GET, POST");
|
|
4304
|
+
res.status(405).end();
|
|
4305
|
+
});
|
|
4306
|
+
baseRouter.get(path, createLogoutNavigationGetHandler(resolvedOptions));
|
|
4307
|
+
baseRouter.post(
|
|
4308
|
+
path,
|
|
4309
|
+
createAsyncHandler("logout", resolvedOptions, async (req, _res, next) => {
|
|
4310
|
+
assertLogoutNavigationRequest(req, resolvedOptions, trustedOrigins);
|
|
4311
|
+
next();
|
|
4312
|
+
})
|
|
4313
|
+
);
|
|
4314
|
+
baseRouter.post(path, express.json({ limit: requestBodyLimit, strict: false }));
|
|
4315
|
+
}
|
|
3520
4316
|
if (resolvedOptions.deviceBinding !== void 0 || resolvedOptions.fingerprintRecognition !== void 0) {
|
|
3521
4317
|
baseRouter.post(
|
|
3522
4318
|
OIDC_VAULT_ROUTE_PATHS.login,
|
|
@@ -3547,9 +4343,14 @@ export {
|
|
|
3547
4343
|
DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT,
|
|
3548
4344
|
OIDC_VAULT_ROUTE_PATHS,
|
|
3549
4345
|
OIDC_VAULT_URL_ENCODED_PARAMETER_LIMIT,
|
|
4346
|
+
OidcVaultAuthDeniedError,
|
|
3550
4347
|
OidcVaultDpopReplayCapacityError,
|
|
4348
|
+
OidcVaultInvalidAccessTokenError,
|
|
4349
|
+
OidcVaultLogoutNavigationCapacityError,
|
|
3551
4350
|
OidcVaultStoreConflictError,
|
|
3552
4351
|
createOidcVaultAccessTokenMiddleware,
|
|
4352
|
+
createOidcVaultAuthenticator,
|
|
4353
|
+
createOidcVaultJwtAccessTokenIssuer,
|
|
3553
4354
|
createOidcVaultJwtAccessTokenValidator,
|
|
3554
4355
|
createOidcVaultMiddleware,
|
|
3555
4356
|
normalizeOidcVaultBasePath,
|