@web-ts-toolkit/express-oidc-vault 0.53.0 → 0.55.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.js
CHANGED
|
@@ -37,9 +37,14 @@ __export(index_exports, {
|
|
|
37
37
|
DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT: () => DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT,
|
|
38
38
|
OIDC_VAULT_ROUTE_PATHS: () => OIDC_VAULT_ROUTE_PATHS,
|
|
39
39
|
OIDC_VAULT_URL_ENCODED_PARAMETER_LIMIT: () => OIDC_VAULT_URL_ENCODED_PARAMETER_LIMIT,
|
|
40
|
+
OidcVaultAuthDeniedError: () => OidcVaultAuthDeniedError,
|
|
40
41
|
OidcVaultDpopReplayCapacityError: () => OidcVaultDpopReplayCapacityError,
|
|
42
|
+
OidcVaultInvalidAccessTokenError: () => OidcVaultInvalidAccessTokenError,
|
|
43
|
+
OidcVaultLogoutNavigationCapacityError: () => OidcVaultLogoutNavigationCapacityError,
|
|
41
44
|
OidcVaultStoreConflictError: () => OidcVaultStoreConflictError,
|
|
42
45
|
createOidcVaultAccessTokenMiddleware: () => createOidcVaultAccessTokenMiddleware,
|
|
46
|
+
createOidcVaultAuthenticator: () => createOidcVaultAuthenticator,
|
|
47
|
+
createOidcVaultJwtAccessTokenIssuer: () => createOidcVaultJwtAccessTokenIssuer,
|
|
43
48
|
createOidcVaultJwtAccessTokenValidator: () => createOidcVaultJwtAccessTokenValidator,
|
|
44
49
|
createOidcVaultMiddleware: () => createOidcVaultMiddleware,
|
|
45
50
|
normalizeOidcVaultBasePath: () => normalizeOidcVaultBasePath,
|
|
@@ -47,7 +52,7 @@ __export(index_exports, {
|
|
|
47
52
|
resolveOidcVaultConfigFromEnv: () => resolveOidcVaultConfigFromEnv
|
|
48
53
|
});
|
|
49
54
|
module.exports = __toCommonJS(index_exports);
|
|
50
|
-
var
|
|
55
|
+
var import_node_crypto9 = require("crypto");
|
|
51
56
|
var import_express = __toESM(require("express"));
|
|
52
57
|
|
|
53
58
|
// src/errors.ts
|
|
@@ -145,6 +150,7 @@ var OIDC_VAULT_ROUTE_PATHS = {
|
|
|
145
150
|
exchange: "/exchange",
|
|
146
151
|
refresh: "/refresh",
|
|
147
152
|
logout: "/logout",
|
|
153
|
+
"logout-navigation": "/logout/navigation",
|
|
148
154
|
"backchannel-logout": "/backchannel-logout"
|
|
149
155
|
};
|
|
150
156
|
|
|
@@ -618,10 +624,11 @@ var invalidProof = (diagnostic, cause) => {
|
|
|
618
624
|
if (cause !== void 0) Object.defineProperty(error, "cause", { value: cause, configurable: true });
|
|
619
625
|
return error;
|
|
620
626
|
};
|
|
621
|
-
var extractDpopProof = (header) => {
|
|
627
|
+
var extractDpopProof = (header, diagnostic) => {
|
|
622
628
|
if (header.count === 0) return void 0;
|
|
623
629
|
const { value } = header;
|
|
624
630
|
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)) {
|
|
631
|
+
diagnostic?.emit("presentation", "rejected", "malformed-proof");
|
|
625
632
|
throw invalidProof("DPoP header must contain one bounded compact signed proof.");
|
|
626
633
|
}
|
|
627
634
|
return value;
|
|
@@ -699,58 +706,105 @@ var publicJwk = (value, alg) => {
|
|
|
699
706
|
return value;
|
|
700
707
|
};
|
|
701
708
|
var verifyDpopProof = async (input) => {
|
|
702
|
-
const { proof, method, targetUrl, proofOptions, expectedJkt, accessToken } = input;
|
|
709
|
+
const { proof, method, targetUrl, proofOptions, expectedJkt, accessToken, diagnostic } = input;
|
|
703
710
|
const algorithms = [...proofOptions.algorithms];
|
|
704
711
|
const { ignoreTargetFailure } = proofOptions;
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
const header = parseObject(decodeBase64url2(encodedHeader, DPOP_PROTECTED_HEADER_MAX_BYTES));
|
|
708
|
-
decodeBase64url2(encodedPayload, DPOP_PROOF_MAX_BYTES);
|
|
709
|
-
const signature = decodeBase64url2(encodedSignature, 512);
|
|
710
|
-
const { alg } = header;
|
|
711
|
-
if (header.typ !== "dpop+jwt" || !isDpopAlgorithm(alg) || !algorithms.includes(alg) || ["crit", "b64", "jku", "x5u", "x5c", "x5t", "x5t#S256"].some((key) => Object.hasOwn(header, key))) {
|
|
712
|
-
throw invalidProof("DPoP protected header has an unsupported type, algorithm or key/critical extension.");
|
|
713
|
-
}
|
|
714
|
-
const jwk = publicJwk(header.jwk, alg);
|
|
715
|
-
const signatureBytes = alg === "ES256" ? 64 : decodeBase64url2(jwk.n, 512).length;
|
|
716
|
-
if (signature.length !== signatureBytes)
|
|
717
|
-
throw invalidProof("DPoP signature length does not match the allowed key profile.");
|
|
718
|
-
let payload;
|
|
719
|
-
try {
|
|
720
|
-
const key = await (0, import_jose.importJWK)(jwk, alg);
|
|
721
|
-
({ payload } = await (0, import_jose.compactVerify)(proof, key, { algorithms }));
|
|
722
|
-
} catch (cause) {
|
|
723
|
-
throw invalidProof("DPoP public-key import or signature verification failed.", cause);
|
|
724
|
-
}
|
|
725
|
-
const claims = parseObject(payload);
|
|
726
|
-
const { htm, htu, iat, jti, nonce, ath } = claims;
|
|
727
|
-
if (typeof iat !== "number" || !Number.isSafeInteger(iat) || iat < 0 || typeof jti !== "string" || jti.length < 1 || jti.length > 128 || /[^\x20-\x7e]/.test(jti)) {
|
|
728
|
-
throw invalidProof("DPoP signed iat/JTI must be nonnegative integer time and bounded printable ASCII.");
|
|
729
|
-
}
|
|
712
|
+
let stage = "header";
|
|
713
|
+
let reason = "malformed-proof";
|
|
730
714
|
try {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
715
|
+
extractDpopProof({ count: 1, value: proof }, diagnostic);
|
|
716
|
+
const [encodedHeader, encodedPayload, encodedSignature] = proof.split(".");
|
|
717
|
+
const header = parseObject(decodeBase64url2(encodedHeader, DPOP_PROTECTED_HEADER_MAX_BYTES));
|
|
718
|
+
decodeBase64url2(encodedPayload, DPOP_PROOF_MAX_BYTES);
|
|
719
|
+
const signature = decodeBase64url2(encodedSignature, 512);
|
|
720
|
+
const { alg } = header;
|
|
721
|
+
reason = "unsupported-header";
|
|
722
|
+
if (header.typ !== "dpop+jwt" || !isDpopAlgorithm(alg) || !algorithms.includes(alg) || ["crit", "b64", "jku", "x5u", "x5c", "x5t", "x5t#S256"].some((key) => Object.hasOwn(header, key))) {
|
|
723
|
+
throw invalidProof("DPoP protected header has an unsupported type, algorithm or key/critical extension.");
|
|
724
|
+
}
|
|
725
|
+
diagnostic?.emit("header", "accepted", "check-passed");
|
|
726
|
+
stage = "key";
|
|
727
|
+
reason = "invalid-key";
|
|
728
|
+
const jwk = publicJwk(header.jwk, alg);
|
|
729
|
+
stage = "signature";
|
|
730
|
+
reason = "invalid-signature";
|
|
731
|
+
const signatureBytes = alg === "ES256" ? 64 : decodeBase64url2(jwk.n, 512).length;
|
|
732
|
+
if (signature.length !== signatureBytes)
|
|
733
|
+
throw invalidProof("DPoP signature length does not match the allowed key profile.");
|
|
734
|
+
let payload;
|
|
735
|
+
try {
|
|
736
|
+
stage = "key";
|
|
737
|
+
reason = "invalid-key";
|
|
738
|
+
const key = await (0, import_jose.importJWK)(jwk, alg);
|
|
739
|
+
diagnostic?.emit("key", "accepted", "check-passed");
|
|
740
|
+
stage = "signature";
|
|
741
|
+
reason = "invalid-signature";
|
|
742
|
+
({ payload } = await (0, import_jose.compactVerify)(proof, key, { algorithms }));
|
|
743
|
+
} catch (cause) {
|
|
744
|
+
throw invalidProof("DPoP public-key import or signature verification failed.", cause);
|
|
745
|
+
}
|
|
746
|
+
diagnostic?.mark({ signature: "verified" });
|
|
747
|
+
diagnostic?.emit("signature", "accepted", "check-passed");
|
|
748
|
+
stage = "claims";
|
|
749
|
+
reason = "invalid-claims";
|
|
750
|
+
const claims = parseObject(payload);
|
|
751
|
+
const { htm, htu, iat, jti, nonce, ath } = claims;
|
|
752
|
+
if (typeof iat !== "number" || !Number.isSafeInteger(iat) || iat < 0 || typeof jti !== "string" || jti.length < 1 || jti.length > 128 || /[^\x20-\x7e]/.test(jti)) {
|
|
753
|
+
throw invalidProof("DPoP signed iat/JTI must be nonnegative integer time and bounded printable ASCII.");
|
|
754
|
+
}
|
|
755
|
+
diagnostic?.signedTime(iat);
|
|
756
|
+
diagnostic?.emit("claims", "accepted", "check-passed");
|
|
757
|
+
stage = "target";
|
|
758
|
+
reason = "target-mismatch";
|
|
759
|
+
try {
|
|
760
|
+
const expectedTarget = typeof targetUrl === "function" ? targetUrl() : targetUrl;
|
|
761
|
+
if (htm !== method || typeof method !== "string" || method.length === 0 || normalizeDpopProofTarget(htu) !== expectedTarget) {
|
|
762
|
+
throw invalidProof("DPoP signed method or normalized public target does not match this request.");
|
|
763
|
+
}
|
|
764
|
+
diagnostic?.mark({ target: "matched" });
|
|
765
|
+
diagnostic?.emit("target", "accepted", "check-passed");
|
|
766
|
+
} catch (error) {
|
|
767
|
+
if (ignoreTargetFailure) {
|
|
768
|
+
diagnostic?.mark({ target: "ignored" });
|
|
769
|
+
diagnostic?.emit("target", "ignored", reason);
|
|
770
|
+
}
|
|
771
|
+
handleDpopCheckFailure(ignoreTargetFailure, "target", error);
|
|
772
|
+
}
|
|
773
|
+
stage = "nonce";
|
|
774
|
+
reason = "invalid-nonce";
|
|
775
|
+
if (typeof nonce === "string" && (nonce.length > DPOP_NONCE_MAX_BYTES || Buffer.byteLength(nonce, "utf8") > DPOP_NONCE_MAX_BYTES)) {
|
|
776
|
+
throw invalidProof("DPoP signed nonce exceeds its byte bound.");
|
|
734
777
|
}
|
|
778
|
+
stage = "token-hash";
|
|
779
|
+
reason = "token-hash-mismatch";
|
|
780
|
+
if (accessToken !== void 0 && (typeof accessToken !== "string" || !/^[\x21-\x7e]+$/.test(accessToken) || !isCanonicalDpopJkt(ath) || ath !== (0, import_node_crypto2.createHash)("sha256").update(accessToken, "ascii").digest("base64url"))) {
|
|
781
|
+
throw invalidProof("DPoP signed access-token hash does not match the presented API token.");
|
|
782
|
+
}
|
|
783
|
+
diagnostic?.mark({ tokenHash: accessToken === void 0 ? "not-applicable" : "matched" });
|
|
784
|
+
if (accessToken !== void 0) diagnostic?.emit("token-hash", "accepted", "check-passed");
|
|
785
|
+
stage = "binding";
|
|
786
|
+
reason = "key-mismatch";
|
|
787
|
+
const jkt = await (0, import_jose.calculateJwkThumbprint)(jwk, "sha256");
|
|
788
|
+
if (expectedJkt !== void 0 && (!isCanonicalDpopJkt(expectedJkt) || jkt !== expectedJkt)) {
|
|
789
|
+
throw invalidProof("DPoP verified public key does not match the original credential confirmation.");
|
|
790
|
+
}
|
|
791
|
+
diagnostic?.mark({ credentialBinding: expectedJkt === void 0 ? "not-applicable" : "matched" });
|
|
792
|
+
if (expectedJkt !== void 0) diagnostic?.emit("binding", "accepted", "check-passed");
|
|
793
|
+
return Object.freeze({
|
|
794
|
+
binding: Object.freeze({ type: "dpop", jkt, alg }),
|
|
795
|
+
iat,
|
|
796
|
+
jti,
|
|
797
|
+
...nonce === void 0 ? {} : { nonce }
|
|
798
|
+
});
|
|
735
799
|
} catch (error) {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
throw
|
|
743
|
-
}
|
|
744
|
-
const jkt = await (0, import_jose.calculateJwkThumbprint)(jwk, "sha256");
|
|
745
|
-
if (expectedJkt !== void 0 && (!isCanonicalDpopJkt(expectedJkt) || jkt !== expectedJkt)) {
|
|
746
|
-
throw invalidProof("DPoP verified public key does not match the original credential confirmation.");
|
|
800
|
+
if (stage === "signature") diagnostic?.mark({ signature: "failed" });
|
|
801
|
+
else if (stage === "target") diagnostic?.mark({ target: "failed" });
|
|
802
|
+
else if (stage === "token-hash") diagnostic?.mark({ tokenHash: "failed" });
|
|
803
|
+
else if (stage === "binding") diagnostic?.mark({ credentialBinding: "failed" });
|
|
804
|
+
else if (stage === "nonce") diagnostic?.mark({ nonce: "failed" });
|
|
805
|
+
diagnostic?.emit(stage, "rejected", reason);
|
|
806
|
+
throw error;
|
|
747
807
|
}
|
|
748
|
-
return Object.freeze({
|
|
749
|
-
binding: Object.freeze({ type: "dpop", jkt, alg }),
|
|
750
|
-
iat,
|
|
751
|
-
jti,
|
|
752
|
-
...nonce === void 0 ? {} : { nonce }
|
|
753
|
-
});
|
|
754
808
|
};
|
|
755
809
|
|
|
756
810
|
// src/fingerprint-recognition.ts
|
|
@@ -813,11 +867,11 @@ var snapshotFingerprintRecognition = (metadata) => {
|
|
|
813
867
|
const value = metadata?.[FINGERPRINT_RECOGNITION_METADATA_KEY];
|
|
814
868
|
if (value === void 0) return void 0;
|
|
815
869
|
if (!isPlainRecord2(value)) throw new TypeError("Stored fingerprint recognition metadata is invalid.");
|
|
816
|
-
const { version, hash } = value;
|
|
817
|
-
if (Object.keys(value).length !== 2 || !Object.hasOwn(value, "version") || !Object.hasOwn(value, "hash") || version !== 1 || !isCanonicalDpopJkt(
|
|
870
|
+
const { version, hash: hash2 } = value;
|
|
871
|
+
if (Object.keys(value).length !== 2 || !Object.hasOwn(value, "version") || !Object.hasOwn(value, "hash") || version !== 1 || !isCanonicalDpopJkt(hash2)) {
|
|
818
872
|
throw new TypeError("Stored fingerprint recognition metadata is invalid.");
|
|
819
873
|
}
|
|
820
|
-
return Object.freeze({ version, hash });
|
|
874
|
+
return Object.freeze({ version, hash: hash2 });
|
|
821
875
|
};
|
|
822
876
|
var fingerprintRecognitionMatches = (actual, original) => actual?.hash === original?.hash;
|
|
823
877
|
var assertFingerprintRecognition = (policy, original, supplied) => {
|
|
@@ -1153,6 +1207,249 @@ function resolveOidcVaultConfigFromEnv(env = process.env) {
|
|
|
1153
1207
|
});
|
|
1154
1208
|
}
|
|
1155
1209
|
|
|
1210
|
+
// src/diagnostics.ts
|
|
1211
|
+
var import_node_crypto5 = require("crypto");
|
|
1212
|
+
var createDiagnosticDispatcher = (observer) => {
|
|
1213
|
+
let pending = 0;
|
|
1214
|
+
let invoking = false;
|
|
1215
|
+
return (event) => {
|
|
1216
|
+
if (!observer || invoking || pending >= 32) return;
|
|
1217
|
+
invoking = true;
|
|
1218
|
+
try {
|
|
1219
|
+
const result = observer(event);
|
|
1220
|
+
if (result !== void 0) {
|
|
1221
|
+
pending++;
|
|
1222
|
+
const release = () => {
|
|
1223
|
+
pending--;
|
|
1224
|
+
};
|
|
1225
|
+
void Promise.resolve(result).then(release, release);
|
|
1226
|
+
}
|
|
1227
|
+
} catch {
|
|
1228
|
+
} finally {
|
|
1229
|
+
invoking = false;
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
};
|
|
1233
|
+
var reference = (purpose, value) => typeof value === "string" && value.length > 0 && value.length <= 1024 ? (0, import_node_crypto5.createHash)("sha256").update(`${purpose}\0${value}`, "utf8").digest("base64url") : void 0;
|
|
1234
|
+
var diagnosticCorrelation = (association, subject, sessionId) => {
|
|
1235
|
+
const subjectRef = reference("subject", subject);
|
|
1236
|
+
const sessionRef = reference("session", sessionId);
|
|
1237
|
+
return Object.freeze({ association, ...subjectRef ? { subjectRef } : {}, ...sessionRef ? { sessionRef } : {} });
|
|
1238
|
+
};
|
|
1239
|
+
var DpopDiagnostic = class {
|
|
1240
|
+
constructor(operation, dispatch) {
|
|
1241
|
+
this.operation = operation;
|
|
1242
|
+
this.dispatch = dispatch;
|
|
1243
|
+
this.verification = {
|
|
1244
|
+
signature: "unchecked",
|
|
1245
|
+
target: "unchecked",
|
|
1246
|
+
tokenHash: "unchecked",
|
|
1247
|
+
credentialBinding: "unchecked",
|
|
1248
|
+
freshness: "unchecked",
|
|
1249
|
+
nonce: "unchecked",
|
|
1250
|
+
replay: "unchecked"
|
|
1251
|
+
};
|
|
1252
|
+
this.terminal = false;
|
|
1253
|
+
}
|
|
1254
|
+
associate(correlation) {
|
|
1255
|
+
this.correlation = correlation;
|
|
1256
|
+
}
|
|
1257
|
+
mark(update) {
|
|
1258
|
+
this.verification = { ...this.verification, ...update };
|
|
1259
|
+
}
|
|
1260
|
+
signedTime(iatSeconds) {
|
|
1261
|
+
this.timing = Object.freeze({ provenance: "signature-verified", iatSeconds });
|
|
1262
|
+
}
|
|
1263
|
+
checkTime(checkedAtMs, clock) {
|
|
1264
|
+
if (!this.timing) return;
|
|
1265
|
+
const issuedAt = this.timing.iatSeconds * 1e3;
|
|
1266
|
+
const age = checkedAtMs - issuedAt;
|
|
1267
|
+
this.timing = Object.freeze({
|
|
1268
|
+
...this.timing,
|
|
1269
|
+
clock,
|
|
1270
|
+
checkedAtMs,
|
|
1271
|
+
...Number.isSafeInteger(issuedAt) && Number.isSafeInteger(age) ? { observedAgeMs: age } : {}
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
emit(stage, outcome, reason) {
|
|
1275
|
+
if (this.terminal) return;
|
|
1276
|
+
if (outcome === "rejected" || outcome === "challenged" || stage === "proof" || stage === "presentation" && outcome === "ignored")
|
|
1277
|
+
this.terminal = true;
|
|
1278
|
+
this.dispatch(
|
|
1279
|
+
Object.freeze({
|
|
1280
|
+
operation: this.operation,
|
|
1281
|
+
stage,
|
|
1282
|
+
outcome,
|
|
1283
|
+
reason,
|
|
1284
|
+
verification: Object.freeze({ ...this.verification }),
|
|
1285
|
+
...this.timing ? { timing: this.timing } : {},
|
|
1286
|
+
...this.correlation ? { correlation: this.correlation } : {}
|
|
1287
|
+
})
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
accept() {
|
|
1291
|
+
if (this.correlation && this.hasAuthenticatedProof()) {
|
|
1292
|
+
this.correlation = Object.freeze({ ...this.correlation, association: "authenticated" });
|
|
1293
|
+
}
|
|
1294
|
+
this.emit("proof", "accepted", "proof-accepted");
|
|
1295
|
+
}
|
|
1296
|
+
/** Policy acceptance with an ignored security check is not strict identity evidence. */
|
|
1297
|
+
hasAuthenticatedProof() {
|
|
1298
|
+
const v = this.verification;
|
|
1299
|
+
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";
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
var diagnosticPreflightReason = (code) => {
|
|
1303
|
+
switch (code) {
|
|
1304
|
+
case "OIDC_VAULT_DPOP_REQUIRED":
|
|
1305
|
+
return "proof-required";
|
|
1306
|
+
case "OIDC_VAULT_DEVICE_BINDING_REQUIRED":
|
|
1307
|
+
return "binding-required";
|
|
1308
|
+
case "OIDC_VAULT_INVALID_SESSION":
|
|
1309
|
+
return "session-mismatch";
|
|
1310
|
+
case "OIDC_VAULT_INVALID_EXCHANGE_CODE":
|
|
1311
|
+
return "session-unavailable";
|
|
1312
|
+
case "OIDC_VAULT_INVALID_ORIGIN":
|
|
1313
|
+
case "OIDC_VAULT_UNTRUSTED_ORIGIN":
|
|
1314
|
+
return "origin-rejected";
|
|
1315
|
+
case "OIDC_VAULT_INVALID_BROWSER_BINDING":
|
|
1316
|
+
return "browser-binding-rejected";
|
|
1317
|
+
case "OIDC_VAULT_FINGERPRINT_REAUTH_REQUIRED":
|
|
1318
|
+
return "recognition-rejected";
|
|
1319
|
+
default:
|
|
1320
|
+
return "preflight-failed";
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
var createDpopDiagnosticFactory = (observer) => {
|
|
1324
|
+
const dispatch = createDiagnosticDispatcher(observer);
|
|
1325
|
+
return (operation) => new DpopDiagnostic(operation, dispatch);
|
|
1326
|
+
};
|
|
1327
|
+
|
|
1328
|
+
// src/logout-navigation.ts
|
|
1329
|
+
var import_node_crypto6 = require("crypto");
|
|
1330
|
+
var import_node_net2 = require("net");
|
|
1331
|
+
|
|
1332
|
+
// src/types.ts
|
|
1333
|
+
var OidcVaultStoreConflictError = class extends Error {
|
|
1334
|
+
constructor(message = "OIDC vault store operation conflicted with concurrent state changes.") {
|
|
1335
|
+
super(message);
|
|
1336
|
+
this.name = "OidcVaultStoreConflictError";
|
|
1337
|
+
}
|
|
1338
|
+
};
|
|
1339
|
+
var OidcVaultDpopReplayCapacityError = class extends Error {
|
|
1340
|
+
constructor(message = "OIDC vault DPoP replay store capacity is exhausted.") {
|
|
1341
|
+
super(message);
|
|
1342
|
+
this.name = "OidcVaultDpopReplayCapacityError";
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
var OidcVaultLogoutNavigationCapacityError = class extends Error {
|
|
1346
|
+
constructor(message = "OIDC vault logout navigation ticket store capacity is exhausted.") {
|
|
1347
|
+
super(message);
|
|
1348
|
+
this.name = "OidcVaultLogoutNavigationCapacityError";
|
|
1349
|
+
}
|
|
1350
|
+
};
|
|
1351
|
+
|
|
1352
|
+
// src/logout-navigation.ts
|
|
1353
|
+
var UNSAFE_URL = /[\s\\\u0000-\u001f\u007f-\u009f]/u;
|
|
1354
|
+
var TICKET = /^lnt_[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;
|
|
1355
|
+
var hash = (value) => (0, import_node_crypto6.createHash)("sha256").update(value, "utf8").digest("base64url");
|
|
1356
|
+
var plainRecord = (value) => typeof value === "object" && value !== null && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
1357
|
+
var destination = (value, name, maxBytes, secure) => {
|
|
1358
|
+
const invalid = () => new TypeError(`${name} must be a bounded absolute HTTP(S) URL without unsafe characters, userinfo or fragment.`);
|
|
1359
|
+
if (typeof value !== "string" || !/^https?:\/\//i.test(value) || /^https?:\/\/[^/?#]*@/i.test(value) || UNSAFE_URL.test(value) || value.includes("#") || Buffer.byteLength(value, "utf8") > maxBytes)
|
|
1360
|
+
throw invalid();
|
|
1361
|
+
let url;
|
|
1362
|
+
try {
|
|
1363
|
+
url = new URL(value);
|
|
1364
|
+
} catch {
|
|
1365
|
+
throw invalid();
|
|
1366
|
+
}
|
|
1367
|
+
const loopback = url.hostname === "localhost" || url.hostname === "[::1]" || (0, import_node_net2.isIP)(url.hostname) === 4 && url.hostname.startsWith("127.");
|
|
1368
|
+
if (url.protocol !== "https:" && url.protocol !== "http:" || secure && url.protocol === "http:" && !loopback || url.username || url.password || url.hash || Buffer.byteLength(url.href, "utf8") > maxBytes)
|
|
1369
|
+
throw invalid();
|
|
1370
|
+
return url;
|
|
1371
|
+
};
|
|
1372
|
+
var resolveLogoutNavigationOptions = (value, input) => {
|
|
1373
|
+
if (value === void 0) return void 0;
|
|
1374
|
+
if (!plainRecord(value)) throw new TypeError("logoutNavigation must be a plain options object.");
|
|
1375
|
+
if (!input.deviceBindingConfigured) throw new TypeError("logoutNavigation requires deviceBinding.");
|
|
1376
|
+
for (const name of ["createLogoutNavigationTicket", "consumeLogoutNavigationTicket"]) {
|
|
1377
|
+
if (typeof input.store[name] !== "function")
|
|
1378
|
+
throw new TypeError(`logoutNavigation requires storeProvider.${name}.`);
|
|
1379
|
+
}
|
|
1380
|
+
const { fallbackRedirectUri, ticketTtlMs = 6e4, namespace = "default" } = value;
|
|
1381
|
+
if (!Number.isInteger(ticketTtlMs) || ticketTtlMs < 1e3 || ticketTtlMs > 12e4)
|
|
1382
|
+
throw new TypeError("logoutNavigation.ticketTtlMs must be an integer from 1000 through 120000.");
|
|
1383
|
+
computeExpiresAt(input.now, ticketTtlMs, "logoutNavigation.ticketTtlMs");
|
|
1384
|
+
if (typeof namespace !== "string" || !/^[\x21-\x7e]{1,128}$/.test(namespace))
|
|
1385
|
+
throw new TypeError("logoutNavigation.namespace must be a static printable ASCII label of 1 through 128 bytes.");
|
|
1386
|
+
const path = `${input.basePath}${OIDC_VAULT_ROUTE_PATHS["logout-navigation"]}`;
|
|
1387
|
+
const fallback = destination(fallbackRedirectUri, "logoutNavigation.fallbackRedirectUri", 2048, true);
|
|
1388
|
+
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
|
|
1389
|
+
// a trailing slash. Those aliases are continuation loops as well.
|
|
1390
|
+
fallback.origin === input.backendOrigin && fallback.pathname.replace(/\/$/, "").toLowerCase() === path.toLowerCase())
|
|
1391
|
+
throw new TypeError(
|
|
1392
|
+
"logoutNavigation.fallbackRedirectUri must stay on the backend/frontend origin and cannot loop to logout navigation."
|
|
1393
|
+
);
|
|
1394
|
+
const postLogout = input.postLogoutRedirectUri === void 0 ? void 0 : destination(input.postLogoutRedirectUri, "logoutNavigation.postLogoutRedirectUri", 2048, true).href;
|
|
1395
|
+
return Object.freeze({
|
|
1396
|
+
fallbackRedirectUri: fallback.href,
|
|
1397
|
+
ticketTtlMs,
|
|
1398
|
+
namespace: "logout-navigation:v1:" + hash(
|
|
1399
|
+
JSON.stringify([
|
|
1400
|
+
"oidc-vault-logout-navigation",
|
|
1401
|
+
1,
|
|
1402
|
+
input.backendOrigin,
|
|
1403
|
+
input.basePath,
|
|
1404
|
+
input.config.issuer,
|
|
1405
|
+
input.config.clientId,
|
|
1406
|
+
namespace,
|
|
1407
|
+
fallback.href,
|
|
1408
|
+
postLogout ?? null
|
|
1409
|
+
])
|
|
1410
|
+
),
|
|
1411
|
+
navigationUrl: `${input.backendOrigin}${path}`,
|
|
1412
|
+
...postLogout === void 0 ? {} : { postLogoutRedirectUri: postLogout }
|
|
1413
|
+
});
|
|
1414
|
+
};
|
|
1415
|
+
var validateUpstreamLogoutUrl = (value) => destination(value, "Logout navigation upstream destination", 16384, false).href;
|
|
1416
|
+
var persistLogoutNavigation = async (policy, store, upstreamLogoutUrl, now) => {
|
|
1417
|
+
const expiresAt = computeExpiresAt(now, policy.ticketTtlMs, "logoutNavigation.ticketTtlMs");
|
|
1418
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
1419
|
+
const ticket = `lnt_${(0, import_node_crypto6.randomBytes)(32).toString("base64url")}`;
|
|
1420
|
+
try {
|
|
1421
|
+
await store.createLogoutNavigationTicket({
|
|
1422
|
+
namespace: policy.namespace,
|
|
1423
|
+
ticketHash: hash(ticket),
|
|
1424
|
+
upstreamLogoutUrl,
|
|
1425
|
+
createdAt: now,
|
|
1426
|
+
expiresAt
|
|
1427
|
+
});
|
|
1428
|
+
return `${policy.navigationUrl}?ticket=${ticket}`;
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
const conflict = error instanceof OidcVaultStoreConflictError || typeof error === "object" && error !== null && "name" in error && error.name === "OidcVaultStoreConflictError";
|
|
1431
|
+
if (!conflict || attempt === 2) throw error;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
throw new Error("Logout navigation ticket attempts exhausted.");
|
|
1435
|
+
};
|
|
1436
|
+
var navigationTicketHash = (originalUrl) => {
|
|
1437
|
+
const index = originalUrl.indexOf("?");
|
|
1438
|
+
if (index < 0) return void 0;
|
|
1439
|
+
const query = originalUrl.slice(index + 1);
|
|
1440
|
+
if (!query.startsWith("ticket=")) return void 0;
|
|
1441
|
+
const ticket = query.slice("ticket=".length);
|
|
1442
|
+
return TICKET.test(ticket) ? hash(ticket) : void 0;
|
|
1443
|
+
};
|
|
1444
|
+
var consumedLogoutDestination = (value, namespace, ticketHash, now) => {
|
|
1445
|
+
if (!plainRecord(value) || Object.keys(value).length !== 5)
|
|
1446
|
+
throw new TypeError("Logout navigation consume returned an invalid record.");
|
|
1447
|
+
const { namespace: returnedNamespace, ticketHash: returnedHash, upstreamLogoutUrl, createdAt, expiresAt } = value;
|
|
1448
|
+
if (returnedNamespace !== namespace || returnedHash !== ticketHash || !isUsableEpochMs(now) || !isUsableEpochMs(createdAt) || !isUsableEpochMs(expiresAt) || expiresAt - createdAt < 1e3 || expiresAt - createdAt > 12e4 || expiresAt <= now || expiresAt - now > 12e4)
|
|
1449
|
+
throw new TypeError("Logout navigation consume returned invalid identity or lifetime.");
|
|
1450
|
+
return validateUpstreamLogoutUrl(upstreamLogoutUrl);
|
|
1451
|
+
};
|
|
1452
|
+
|
|
1156
1453
|
// src/utils.ts
|
|
1157
1454
|
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
1158
1455
|
var isString = (value) => typeof value === "string" && value.length > 0;
|
|
@@ -1237,8 +1534,8 @@ var getRequestSourceOrigin = (req) => {
|
|
|
1237
1534
|
var assertTrustedOrigin = (req, options, trustedOrigins, action) => {
|
|
1238
1535
|
captureTrustedOriginGuard(req, options, trustedOrigins, action)();
|
|
1239
1536
|
};
|
|
1240
|
-
var captureTrustedOriginGuard = (req, options, trustedOrigins, action) => {
|
|
1241
|
-
const guarded = action === "login" || action === "exchange";
|
|
1537
|
+
var captureTrustedOriginGuard = (req, options, trustedOrigins, action, strict = false) => {
|
|
1538
|
+
const guarded = strict || action === "login" || action === "exchange";
|
|
1242
1539
|
if (!guarded && !usesCookieTransport(options)) {
|
|
1243
1540
|
return () => {
|
|
1244
1541
|
};
|
|
@@ -2006,6 +2303,28 @@ var createExchangeResponse = (options, session, issuedToken) => ({
|
|
|
2006
2303
|
expiresIn: issuedToken.expiresIn,
|
|
2007
2304
|
tokenType: issuedToken.tokenType
|
|
2008
2305
|
});
|
|
2306
|
+
var notifyTokenOutcomes = (operation, session, issuedToken, notify, diagnostic) => {
|
|
2307
|
+
const deviceBound = session.deviceBinding !== void 0;
|
|
2308
|
+
const correlation = diagnosticCorrelation(
|
|
2309
|
+
deviceBound && !diagnostic?.hasAuthenticatedProof() ? "candidate-session" : "authenticated",
|
|
2310
|
+
session.subject,
|
|
2311
|
+
session.logicalSessionId ?? session.sessionId
|
|
2312
|
+
);
|
|
2313
|
+
const tokenIssued = issuedToken.accessToken !== void 0;
|
|
2314
|
+
if (tokenIssued)
|
|
2315
|
+
notify(
|
|
2316
|
+
Object.freeze({
|
|
2317
|
+
kind: "token-issued",
|
|
2318
|
+
operation,
|
|
2319
|
+
correlation,
|
|
2320
|
+
deviceBound,
|
|
2321
|
+
expiresIn: issuedToken.expiresIn,
|
|
2322
|
+
...issuedToken.tokenType === void 0 ? {} : { tokenType: issuedToken.tokenType }
|
|
2323
|
+
})
|
|
2324
|
+
);
|
|
2325
|
+
if (operation === "exchange")
|
|
2326
|
+
notify(Object.freeze({ kind: "exchange-succeeded", operation, correlation, deviceBound, tokenIssued }));
|
|
2327
|
+
};
|
|
2009
2328
|
var assertIssuedBoundJwt = (accessToken, jkt) => {
|
|
2010
2329
|
if (!isCompactSignedJwt(accessToken)) {
|
|
2011
2330
|
throw new TypeError("tokenIssuer.issue accessToken must be a compact signed JWT for bound issuance.");
|
|
@@ -2032,13 +2351,14 @@ var assertIssuedBoundJwt = (accessToken, jkt) => {
|
|
|
2032
2351
|
throw new TypeError("tokenIssuer.issue accessToken exp must be a nonnegative safe integer with safe milliseconds.");
|
|
2033
2352
|
}
|
|
2034
2353
|
};
|
|
2035
|
-
var withIssuedToken = async (req, res, options, session, verifiedBinding) => {
|
|
2354
|
+
var withIssuedToken = async (req, res, options, session, operation, verifiedBinding) => {
|
|
2036
2355
|
const binding = resolveVerifiedDeviceBinding(options.deviceBinding, session.deviceBinding, verifiedBinding);
|
|
2037
2356
|
if (!options.tokenIssuer) return {};
|
|
2038
2357
|
const logicalSessionId = session.logicalSessionId ?? session.sessionId;
|
|
2039
2358
|
try {
|
|
2040
2359
|
const issuerSession = withSessionDeviceBinding(copyVaultSession(session, true), binding);
|
|
2041
2360
|
const result = await options.tokenIssuer.issue({
|
|
2361
|
+
operation,
|
|
2042
2362
|
req,
|
|
2043
2363
|
res,
|
|
2044
2364
|
session: issuerSession,
|
|
@@ -2078,6 +2398,31 @@ var withIssuedToken = async (req, res, options, session, verifiedBinding) => {
|
|
|
2078
2398
|
|
|
2079
2399
|
// src/token-validation.ts
|
|
2080
2400
|
var import_jose4 = require("jose");
|
|
2401
|
+
|
|
2402
|
+
// src/access-token-errors.ts
|
|
2403
|
+
var OidcVaultInvalidAccessTokenError = class extends OidcVaultHttpError {
|
|
2404
|
+
constructor(message = "Access token rejected.") {
|
|
2405
|
+
super(401, "OIDC_VAULT_INVALID_ACCESS_TOKEN", message, "Access token validation failed.");
|
|
2406
|
+
this.name = "OidcVaultInvalidAccessTokenError";
|
|
2407
|
+
}
|
|
2408
|
+
};
|
|
2409
|
+
var OidcVaultAuthDeniedError = class extends OidcVaultHttpError {
|
|
2410
|
+
constructor(message = "Authentication denied.", status = 403) {
|
|
2411
|
+
if (status !== 401 && status !== 403) throw new TypeError("Auth denial status must be 401 or 403.");
|
|
2412
|
+
super(status, "OIDC_VAULT_AUTH_DENIED", message);
|
|
2413
|
+
this.name = "OidcVaultAuthDeniedError";
|
|
2414
|
+
}
|
|
2415
|
+
};
|
|
2416
|
+
var credentialOutcomes = /* @__PURE__ */ new WeakMap();
|
|
2417
|
+
var markAccessTokenRejected = (error) => {
|
|
2418
|
+
if (typeof error === "object" && error !== null) credentialOutcomes.set(error, true);
|
|
2419
|
+
};
|
|
2420
|
+
var markAccessTokenValidationFailure = (error) => {
|
|
2421
|
+
if (typeof error === "object" && error !== null) credentialOutcomes.set(error, false);
|
|
2422
|
+
};
|
|
2423
|
+
var isAccessTokenRejected = (error) => typeof error === "object" && error !== null && (credentialOutcomes.get(error) ?? error instanceof OidcVaultInvalidAccessTokenError);
|
|
2424
|
+
|
|
2425
|
+
// src/token-validation.ts
|
|
2081
2426
|
var BACKCHANNEL_LOGOUT_EVENT_CLAIM = "http://schemas.openid.net/event/backchannel-logout";
|
|
2082
2427
|
var validateBackchannelLogoutTokenTyp = (typ) => {
|
|
2083
2428
|
if (typ !== void 0 && (typeof typ !== "string" || typ.toLowerCase() !== "logout+jwt")) {
|
|
@@ -2236,20 +2581,36 @@ function createOidcVaultJwtAccessTokenValidator(options) {
|
|
|
2236
2581
|
const audience = Array.isArray(configuredAudience) ? [...configuredAudience] : configuredAudience;
|
|
2237
2582
|
const algorithms = configuredAlgorithms === void 0 ? void 0 : [...configuredAlgorithms];
|
|
2238
2583
|
const validateJwt = async (token) => {
|
|
2239
|
-
const result = await (0, import_jose4.jwtVerify)(token, key, { issuer, audience, algorithms })
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
if (confirmation !== null) {
|
|
2243
|
-
if (!isCompactSignedJwt(token)) {
|
|
2244
|
-
throw invalidAccessToken("Bound JWT must use canonical compact signed encoding.");
|
|
2584
|
+
const result = await (0, import_jose4.jwtVerify)(token, key, { issuer, audience, algorithms }).catch((error) => {
|
|
2585
|
+
if (error instanceof import_jose4.errors.JWTExpired || error instanceof import_jose4.errors.JWTClaimValidationFailed || error instanceof import_jose4.errors.JWTInvalid || error instanceof import_jose4.errors.JWSInvalid || error instanceof import_jose4.errors.JWSSignatureVerificationFailed || error instanceof import_jose4.errors.JOSEAlgNotAllowed) {
|
|
2586
|
+
markAccessTokenRejected(error);
|
|
2245
2587
|
}
|
|
2246
|
-
|
|
2247
|
-
|
|
2588
|
+
throw error;
|
|
2589
|
+
});
|
|
2590
|
+
const claims = result.payload;
|
|
2591
|
+
let confirmation;
|
|
2592
|
+
try {
|
|
2593
|
+
confirmation = snapshotVerifiedJwtConfirmation(claims);
|
|
2594
|
+
if (confirmation !== null) {
|
|
2595
|
+
if (!isCompactSignedJwt(token)) {
|
|
2596
|
+
throw invalidAccessToken("Bound JWT must use canonical compact signed encoding.");
|
|
2597
|
+
}
|
|
2598
|
+
if (readJwtExpirationMs(claims.exp) === void 0) {
|
|
2599
|
+
throw invalidAccessToken("Bound JWT exp must be a nonnegative safe integer with safe milliseconds.");
|
|
2600
|
+
}
|
|
2248
2601
|
}
|
|
2602
|
+
if (!mapClaims) return { ...defaultJwtClaimsMapper(claims), confirmation };
|
|
2603
|
+
} catch (error) {
|
|
2604
|
+
markAccessTokenRejected(error);
|
|
2605
|
+
throw error;
|
|
2606
|
+
}
|
|
2607
|
+
try {
|
|
2608
|
+
const { subject, sessionId, scope, claims: mappedClaims } = mapClaims(claims);
|
|
2609
|
+
return { subject, sessionId, scope, claims: mappedClaims, confirmation };
|
|
2610
|
+
} catch (error) {
|
|
2611
|
+
markAccessTokenValidationFailure(error);
|
|
2612
|
+
throw error;
|
|
2249
2613
|
}
|
|
2250
|
-
const mapped = mapClaims ? mapClaims(claims) : defaultJwtClaimsMapper(claims);
|
|
2251
|
-
const { subject, sessionId, scope, claims: mappedClaims } = mapped;
|
|
2252
|
-
return { subject, sessionId, scope, claims: mappedClaims, confirmation };
|
|
2253
2614
|
};
|
|
2254
2615
|
return Object.freeze({
|
|
2255
2616
|
async validate(token) {
|
|
@@ -2264,22 +2625,8 @@ function createOidcVaultJwtAccessTokenValidator(options) {
|
|
|
2264
2625
|
});
|
|
2265
2626
|
}
|
|
2266
2627
|
|
|
2267
|
-
// src/types.ts
|
|
2268
|
-
var OidcVaultStoreConflictError = class extends Error {
|
|
2269
|
-
constructor(message = "OIDC vault store operation conflicted with concurrent state changes.") {
|
|
2270
|
-
super(message);
|
|
2271
|
-
this.name = "OidcVaultStoreConflictError";
|
|
2272
|
-
}
|
|
2273
|
-
};
|
|
2274
|
-
var OidcVaultDpopReplayCapacityError = class extends Error {
|
|
2275
|
-
constructor(message = "OIDC vault DPoP replay store capacity is exhausted.") {
|
|
2276
|
-
super(message);
|
|
2277
|
-
this.name = "OidcVaultDpopReplayCapacityError";
|
|
2278
|
-
}
|
|
2279
|
-
};
|
|
2280
|
-
|
|
2281
2628
|
// src/dpop-replay.ts
|
|
2282
|
-
var
|
|
2629
|
+
var import_node_crypto7 = require("crypto");
|
|
2283
2630
|
var isPrintableIdentifier = (value) => typeof value === "string" && /^[\x20-\x7e]{1,128}$/.test(value);
|
|
2284
2631
|
var normalizeNamespaceBasePath = (origin, value) => {
|
|
2285
2632
|
if (value !== void 0 && typeof value !== "string")
|
|
@@ -2317,7 +2664,7 @@ var createDpopReplayKey = (effectiveNamespace, jkt, jti) => {
|
|
|
2317
2664
|
if (!isCanonicalDpopJkt(jkt) || !isPrintableIdentifier(jti)) {
|
|
2318
2665
|
throw new TypeError("DPoP replay key requires a canonical thumbprint and a bounded printable JTI.");
|
|
2319
2666
|
}
|
|
2320
|
-
return `dpop:v1:${(0,
|
|
2667
|
+
return `dpop:v1:${(0, import_node_crypto7.createHash)("sha256").update(JSON.stringify([effectiveNamespace, jkt, jti]), "utf8").digest("base64url")}`;
|
|
2321
2668
|
};
|
|
2322
2669
|
var getDpopReplayExpiresAt = (input) => {
|
|
2323
2670
|
const { iat, now, proofMaxAgeSeconds: age, clockSkewSeconds: skew } = input;
|
|
@@ -2356,47 +2703,85 @@ var createDpopReplayPolicy = (options) => {
|
|
|
2356
2703
|
});
|
|
2357
2704
|
return Object.freeze({
|
|
2358
2705
|
effectiveNamespace,
|
|
2359
|
-
async verifyAndReserve(verifyBeforeReplay) {
|
|
2706
|
+
async verifyAndReserve(verifyBeforeReplay, diagnostic) {
|
|
2360
2707
|
const verified = await verifyBeforeReplay();
|
|
2361
2708
|
if (typeof verified !== "object" || verified === null || Array.isArray(verified)) {
|
|
2709
|
+
diagnostic?.emit("claims", "rejected", "invalid-claims");
|
|
2362
2710
|
throw invalidProof2("DPoP verifier did not return a proof verification result.");
|
|
2363
2711
|
}
|
|
2364
2712
|
const { binding: verifiedBinding, iat, jti, nonce } = verified;
|
|
2365
2713
|
if (typeof verifiedBinding !== "object" || verifiedBinding === null || Array.isArray(verifiedBinding)) {
|
|
2714
|
+
diagnostic?.emit("binding", "rejected", "invalid-key");
|
|
2366
2715
|
throw invalidProof2("DPoP verifier did not return a verified key binding.");
|
|
2367
2716
|
}
|
|
2368
2717
|
const { type, jkt, alg } = verifiedBinding;
|
|
2369
2718
|
if (type !== "dpop" || !isCanonicalDpopJkt(jkt) || !isDpopAlgorithm(alg) || !policy.algorithms.includes(alg) || !isPrintableIdentifier(jti) || typeof iat !== "number" || !Number.isSafeInteger(iat) || iat < 0) {
|
|
2719
|
+
diagnostic?.emit("claims", "rejected", "invalid-claims");
|
|
2370
2720
|
throw invalidProof2("DPoP verified key/algorithm or signed iat/JTI shape is invalid.");
|
|
2371
2721
|
}
|
|
2372
2722
|
const binding = Object.freeze({ type, jkt, alg });
|
|
2373
2723
|
let checkedNow;
|
|
2724
|
+
let clockSource = configuredNow ? "configured" : "system";
|
|
2374
2725
|
try {
|
|
2375
2726
|
checkedNow = now();
|
|
2376
2727
|
if (!Number.isSafeInteger(checkedNow) || checkedNow < 0) throw new TypeError("DPoP replay clock is invalid.");
|
|
2377
2728
|
} catch (error) {
|
|
2729
|
+
diagnostic?.mark({ freshness: policy.ignoreFreshnessFailure ? "ignored" : "failed" });
|
|
2730
|
+
diagnostic?.emit("freshness", policy.ignoreFreshnessFailure ? "ignored" : "rejected", "clock-unavailable");
|
|
2378
2731
|
handleDpopCheckFailure(policy.ignoreFreshnessFailure, "freshness", replayUnavailable(error));
|
|
2379
2732
|
checkedNow = Date.now();
|
|
2380
|
-
if (!Number.isSafeInteger(checkedNow) || checkedNow < 0)
|
|
2733
|
+
if (!Number.isSafeInteger(checkedNow) || checkedNow < 0) {
|
|
2734
|
+
diagnostic?.mark({ freshness: "failed" });
|
|
2735
|
+
diagnostic?.emit("freshness", "rejected", "clock-unavailable");
|
|
2381
2736
|
throw replayUnavailable(new TypeError("DPoP fallback clock is invalid."));
|
|
2737
|
+
}
|
|
2738
|
+
clockSource = "fallback";
|
|
2382
2739
|
}
|
|
2740
|
+
diagnostic?.checkTime(checkedNow, clockSource);
|
|
2383
2741
|
let expiresAt = getDpopReplayExpiresAt({ ...policy, iat, now: checkedNow });
|
|
2384
2742
|
if (expiresAt === null) {
|
|
2743
|
+
diagnostic?.mark({ freshness: policy.ignoreFreshnessFailure ? "ignored" : "failed" });
|
|
2744
|
+
diagnostic?.emit(
|
|
2745
|
+
"freshness",
|
|
2746
|
+
policy.ignoreFreshnessFailure ? "ignored" : "rejected",
|
|
2747
|
+
!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"
|
|
2748
|
+
);
|
|
2385
2749
|
handleDpopCheckFailure(
|
|
2386
2750
|
policy.ignoreFreshnessFailure,
|
|
2387
2751
|
"freshness",
|
|
2388
2752
|
invalidProof2("DPoP signed iat is outside the bounded replay window.")
|
|
2389
2753
|
);
|
|
2390
2754
|
expiresAt = checkedNow + (policy.proofMaxAgeSeconds + policy.clockSkewSeconds) * 1e3;
|
|
2755
|
+
} else {
|
|
2756
|
+
diagnostic?.mark({ freshness: clockSource === "fallback" ? "ignored" : "passed" });
|
|
2757
|
+
diagnostic?.emit("freshness", "accepted", "check-passed");
|
|
2391
2758
|
}
|
|
2392
2759
|
if (noncePolicy) {
|
|
2393
2760
|
if (!noncePolicy.verify(nonce, jkt, checkedNow)) {
|
|
2394
|
-
|
|
2761
|
+
let challengeNonce;
|
|
2762
|
+
try {
|
|
2763
|
+
challengeNonce = noncePolicy.issue(jkt, checkedNow);
|
|
2764
|
+
} catch (error) {
|
|
2765
|
+
diagnostic?.mark({ nonce: "failed" });
|
|
2766
|
+
diagnostic?.emit("nonce", "rejected", "nonce-unavailable");
|
|
2767
|
+
throw error;
|
|
2768
|
+
}
|
|
2769
|
+
diagnostic?.mark({ nonce: "challenged" });
|
|
2770
|
+
diagnostic?.emit("nonce", "challenged", nonce === void 0 ? "nonce-required" : "nonce-invalid");
|
|
2771
|
+
return Object.freeze({ type: "nonce-challenge", nonce: challengeNonce });
|
|
2395
2772
|
}
|
|
2773
|
+
diagnostic?.mark({ nonce: "passed" });
|
|
2774
|
+
diagnostic?.emit("nonce", "accepted", "check-passed");
|
|
2396
2775
|
} else if (nonce !== void 0 && (typeof nonce !== "string" || nonce.length > DPOP_NONCE_MAX_BYTES || Buffer.byteLength(nonce, "utf8") > DPOP_NONCE_MAX_BYTES)) {
|
|
2776
|
+
diagnostic?.mark({ nonce: "failed" });
|
|
2777
|
+
diagnostic?.emit("nonce", "rejected", "invalid-nonce");
|
|
2397
2778
|
throw invalidProof2("DPoP signed nonce claim exceeds its string/byte bound.");
|
|
2779
|
+
} else {
|
|
2780
|
+
diagnostic?.mark({ nonce: "disabled" });
|
|
2781
|
+
diagnostic?.emit("nonce", "ignored", "nonce-disabled");
|
|
2398
2782
|
}
|
|
2399
2783
|
let reservationError;
|
|
2784
|
+
let unavailable = false;
|
|
2400
2785
|
try {
|
|
2401
2786
|
const reserved = await replayStore.reserveDpopProof({
|
|
2402
2787
|
replayKey: createDpopReplayKey(effectiveNamespace, jkt, jti),
|
|
@@ -2407,8 +2792,21 @@ var createDpopReplayPolicy = (options) => {
|
|
|
2407
2792
|
if (!reserved) reservationError = invalidProof2("DPoP replay reservation was duplicate or no longer live.");
|
|
2408
2793
|
} catch (error) {
|
|
2409
2794
|
reservationError = replayUnavailable(error);
|
|
2795
|
+
unavailable = true;
|
|
2410
2796
|
}
|
|
2411
|
-
if (reservationError)
|
|
2797
|
+
if (reservationError) {
|
|
2798
|
+
diagnostic?.mark({ replay: policy.ignoreReplayFailure ? "ignored" : "failed" });
|
|
2799
|
+
diagnostic?.emit(
|
|
2800
|
+
"replay",
|
|
2801
|
+
policy.ignoreReplayFailure ? "ignored" : "rejected",
|
|
2802
|
+
unavailable ? "replay-unavailable" : "replay-detected"
|
|
2803
|
+
);
|
|
2804
|
+
handleDpopCheckFailure(policy.ignoreReplayFailure, "replay", reservationError);
|
|
2805
|
+
} else {
|
|
2806
|
+
diagnostic?.mark({ replay: "reserved" });
|
|
2807
|
+
diagnostic?.emit("replay", "accepted", "check-passed");
|
|
2808
|
+
}
|
|
2809
|
+
diagnostic?.accept();
|
|
2412
2810
|
return Object.freeze({ type: "accepted", binding, expiresAt });
|
|
2413
2811
|
}
|
|
2414
2812
|
});
|
|
@@ -2447,7 +2845,8 @@ var toDpopNonceChallengeResponse = (result, endpoint, algorithms) => {
|
|
|
2447
2845
|
};
|
|
2448
2846
|
|
|
2449
2847
|
// src/vault-route-proof.ts
|
|
2450
|
-
var captureVaultRouteProof = (req) => Object.freeze({
|
|
2848
|
+
var captureVaultRouteProof = (req, diagnostic) => Object.freeze({
|
|
2849
|
+
diagnostic,
|
|
2451
2850
|
header: readDpopRawHeader(req, "dpop"),
|
|
2452
2851
|
method: req.method,
|
|
2453
2852
|
originalUrl: req.originalUrl
|
|
@@ -2471,14 +2870,17 @@ var createVaultRouteProofVerifier = (options, config, backendOrigin, basePath) =
|
|
|
2471
2870
|
now: options.now
|
|
2472
2871
|
});
|
|
2473
2872
|
const admit = async (request, proof, expectedJkt) => {
|
|
2873
|
+
const { diagnostic } = request;
|
|
2474
2874
|
const result = await replay.verifyAndReserve(
|
|
2475
2875
|
() => verifyDpopProof({
|
|
2476
2876
|
proof,
|
|
2477
2877
|
method: request.method,
|
|
2478
2878
|
targetUrl: () => targetFor(request.originalUrl),
|
|
2479
2879
|
proofOptions: policy,
|
|
2480
|
-
expectedJkt
|
|
2481
|
-
|
|
2880
|
+
expectedJkt,
|
|
2881
|
+
diagnostic
|
|
2882
|
+
}),
|
|
2883
|
+
diagnostic
|
|
2482
2884
|
);
|
|
2483
2885
|
if (result.type === "nonce-challenge") {
|
|
2484
2886
|
throw new VaultDpopNonceChallengeError(toDpopNonceChallengeResponse(result, "vault-post", policy.algorithms));
|
|
@@ -2487,9 +2889,10 @@ var createVaultRouteProofVerifier = (options, config, backendOrigin, basePath) =
|
|
|
2487
2889
|
};
|
|
2488
2890
|
return Object.freeze({
|
|
2489
2891
|
async initiate(request) {
|
|
2490
|
-
const proof = extractDpopProof(request.header);
|
|
2892
|
+
const proof = extractDpopProof(request.header, request.diagnostic);
|
|
2491
2893
|
if (proof === void 0) {
|
|
2492
2894
|
assertDeviceBindingPolicy(policy, void 0);
|
|
2895
|
+
request.diagnostic?.emit("presentation", "ignored", "proof-absent");
|
|
2493
2896
|
return void 0;
|
|
2494
2897
|
}
|
|
2495
2898
|
return admit(request, proof);
|
|
@@ -2497,9 +2900,11 @@ var createVaultRouteProofVerifier = (options, config, backendOrigin, basePath) =
|
|
|
2497
2900
|
async verify(request, originalBinding) {
|
|
2498
2901
|
const binding = snapshotDpopBinding(originalBinding);
|
|
2499
2902
|
assertDeviceBindingPolicy(policy, binding);
|
|
2500
|
-
const proof = extractDpopProof(request.header);
|
|
2903
|
+
const proof = extractDpopProof(request.header, request.diagnostic);
|
|
2501
2904
|
const verified = proof === void 0 ? void 0 : await admit(request, proof, binding?.jkt);
|
|
2502
|
-
|
|
2905
|
+
const resolved = resolveVerifiedDeviceBinding(policy, binding, verified);
|
|
2906
|
+
if (proof === void 0) request.diagnostic?.emit("presentation", "ignored", "proof-absent");
|
|
2907
|
+
return resolved;
|
|
2503
2908
|
}
|
|
2504
2909
|
});
|
|
2505
2910
|
};
|
|
@@ -2517,7 +2922,7 @@ var extractBearerToken = (authorizationHeader) => {
|
|
|
2517
2922
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_MISSING_BEARER_TOKEN", "Missing bearer token.");
|
|
2518
2923
|
}
|
|
2519
2924
|
const [scheme, token, extra] = authorizationHeader.trim().split(/\s+/);
|
|
2520
|
-
if (scheme?.toLowerCase() !== "bearer" || !token || extra) {
|
|
2925
|
+
if (scheme?.toLowerCase() !== "bearer" || !token || extra || !/^[\x21-\x7e]+$/.test(token) || token.includes(",")) {
|
|
2521
2926
|
throw new OidcVaultHttpError(
|
|
2522
2927
|
401,
|
|
2523
2928
|
"OIDC_VAULT_INVALID_AUTHORIZATION_HEADER",
|
|
@@ -2544,8 +2949,40 @@ var mappedFields = (result) => {
|
|
|
2544
2949
|
throw invalidAccessToken("Access token validator result is missing subject.");
|
|
2545
2950
|
return { subject, sessionId, scope, claims };
|
|
2546
2951
|
};
|
|
2952
|
+
var evidence = /* @__PURE__ */ new WeakMap();
|
|
2953
|
+
var authFields = ["subject", "sessionId", "scope", "claims", "token", "confirmation", "deviceBinding"];
|
|
2954
|
+
var credentialRejectionCodes = /* @__PURE__ */ new Set([
|
|
2955
|
+
"OIDC_VAULT_MISSING_BEARER_TOKEN",
|
|
2956
|
+
"OIDC_VAULT_MISSING_ACCESS_TOKEN",
|
|
2957
|
+
"OIDC_VAULT_INVALID_AUTHORIZATION_HEADER",
|
|
2958
|
+
"OIDC_VAULT_INVALID_ACCESS_TOKEN",
|
|
2959
|
+
"OIDC_VAULT_DPOP_REQUIRED",
|
|
2960
|
+
"OIDC_VAULT_DEVICE_BINDING_REQUIRED",
|
|
2961
|
+
"OIDC_VAULT_INVALID_DPOP_PROOF"
|
|
2962
|
+
]);
|
|
2963
|
+
var clearChallenge = (res) => {
|
|
2964
|
+
res.removeHeader("WWW-Authenticate");
|
|
2965
|
+
res.removeHeader("DPoP-Nonce");
|
|
2966
|
+
res.setHeader("Cache-Control", "no-store");
|
|
2967
|
+
};
|
|
2968
|
+
var sendError = (res, response) => {
|
|
2969
|
+
clearChallenge(res);
|
|
2970
|
+
for (const [name, value] of Object.entries(response.headers)) res.setHeader(name, value);
|
|
2971
|
+
res.status(response.status).json(response.body);
|
|
2972
|
+
};
|
|
2547
2973
|
function createOidcVaultAccessTokenMiddleware(options) {
|
|
2548
|
-
|
|
2974
|
+
return createOidcVaultAuthenticator(options).authenticate;
|
|
2975
|
+
}
|
|
2976
|
+
function createOidcVaultAuthenticator(options) {
|
|
2977
|
+
const { validator, deviceBinding: configuredBinding, onAuthContext, onError, onDpopDiagnostic } = options;
|
|
2978
|
+
const createDiagnostic = createDpopDiagnosticFactory(onDpopDiagnostic);
|
|
2979
|
+
const { authentication = "required", invalidCredentials = "reject" } = options;
|
|
2980
|
+
if (authentication !== "required" && authentication !== "optional")
|
|
2981
|
+
throw new TypeError("authentication must be required or optional.");
|
|
2982
|
+
if (invalidCredentials !== "reject" && invalidCredentials !== "anonymous")
|
|
2983
|
+
throw new TypeError("invalidCredentials must be reject or anonymous.");
|
|
2984
|
+
if (invalidCredentials === "anonymous" && authentication !== "optional")
|
|
2985
|
+
throw new TypeError("Anonymous invalid credentials require optional authentication.");
|
|
2549
2986
|
const policy = resolveDeviceBindingOptions(configuredBinding);
|
|
2550
2987
|
const validate = validator.validate.bind(validator);
|
|
2551
2988
|
const requestAware = policy ? validator.validateWithRequest : void 0;
|
|
@@ -2577,56 +3014,134 @@ function createOidcVaultAccessTokenMiddleware(options) {
|
|
|
2577
3014
|
const algs = algorithms.join(" ");
|
|
2578
3015
|
const dpopChallenge = (error) => `DPoP${error ? ` error="${error}",` : ""} algs="${algs}"`;
|
|
2579
3016
|
const proofChallenge = dpopChallenge("invalid_dpop_proof");
|
|
2580
|
-
|
|
3017
|
+
const owner = Object.freeze({});
|
|
3018
|
+
const prepareError = (error, scheme, nonceResponse, hookFailure = false) => {
|
|
3019
|
+
const response = nonceResponse ?? (error instanceof OidcVaultHttpError ? toDpopReplayErrorResponse(error, algorithms) : {
|
|
3020
|
+
status: hookFailure ? 500 : 401,
|
|
3021
|
+
body: hookFailure ? { code: "OIDC_VAULT_AUTH_CONTEXT_FAILED", message: "Auth context hook failed." } : { code: "OIDC_VAULT_INVALID_ACCESS_TOKEN", message: "Access token validation failed." },
|
|
3022
|
+
headers: { "Cache-Control": "no-store" }
|
|
3023
|
+
});
|
|
3024
|
+
const headers = { ...response.headers };
|
|
3025
|
+
if (response.status === 401 && headers["WWW-Authenticate"] === void 0) {
|
|
3026
|
+
const code = response.body.code;
|
|
3027
|
+
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"';
|
|
3028
|
+
headers["WWW-Authenticate"] = challenge2;
|
|
3029
|
+
}
|
|
3030
|
+
return Object.freeze({
|
|
3031
|
+
status: response.status,
|
|
3032
|
+
body: Object.freeze({ ...response.body }),
|
|
3033
|
+
headers: Object.freeze(headers)
|
|
3034
|
+
});
|
|
3035
|
+
};
|
|
3036
|
+
const reentryError = () => new OidcVaultHttpError(500, "OIDC_VAULT_AUTHENTICATION_REENTRY", "Authentication is already running or completed.");
|
|
3037
|
+
const rejectGuard = async (req, res, next) => {
|
|
3038
|
+
const state = evidence.get(req);
|
|
3039
|
+
const error = state?.status === "pending" ? reentryError() : new OidcVaultHttpError(401, "OIDC_VAULT_AUTHENTICATION_REQUIRED", "Authentication is required.");
|
|
3040
|
+
if (state) state.status = "failed";
|
|
3041
|
+
else evidence.set(req, { owner, res, status: "failed" });
|
|
3042
|
+
delete req.auth;
|
|
3043
|
+
const response = prepareError(error);
|
|
3044
|
+
await notifyMiddlewareError(onError, { error, req, res });
|
|
3045
|
+
delete req.auth;
|
|
3046
|
+
if (res.headersSent) next(error);
|
|
3047
|
+
else sendError(res, response);
|
|
3048
|
+
};
|
|
3049
|
+
const guard = (bound) => async (req, res, next) => {
|
|
3050
|
+
const state = evidence.get(req);
|
|
3051
|
+
const current = Object.getOwnPropertyDescriptor(req, "auth");
|
|
3052
|
+
const intact = state?.auth && state.snapshot && current && "value" in current && current.value === state.auth && authFields.every((key) => {
|
|
3053
|
+
const field = Object.getOwnPropertyDescriptor(state.auth, key);
|
|
3054
|
+
return Object.hasOwn(state.snapshot, key) ? field && "value" in field && field.value === state.snapshot[key] : field === void 0;
|
|
3055
|
+
});
|
|
3056
|
+
if (state?.status !== "authenticated" || state.owner !== owner || state.res !== res || !intact) {
|
|
3057
|
+
await rejectGuard(req, res, next);
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
if (bound && !state.snapshot.deviceBinding) {
|
|
3061
|
+
const error = new OidcVaultHttpError(
|
|
3062
|
+
401,
|
|
3063
|
+
"OIDC_VAULT_DEVICE_BINDING_REQUIRED",
|
|
3064
|
+
"A device-bound login is required."
|
|
3065
|
+
);
|
|
3066
|
+
state.status = "failed";
|
|
3067
|
+
delete req.auth;
|
|
3068
|
+
const response = prepareError(error);
|
|
3069
|
+
await notifyMiddlewareError(onError, { error, req, res });
|
|
3070
|
+
delete req.auth;
|
|
3071
|
+
if (res.headersSent) next(error);
|
|
3072
|
+
else sendError(res, response);
|
|
3073
|
+
return;
|
|
3074
|
+
}
|
|
3075
|
+
if (!res.headersSent) res.setHeader("Cache-Control", "no-store");
|
|
3076
|
+
next();
|
|
3077
|
+
};
|
|
3078
|
+
const authenticate = async (req, res, next) => {
|
|
3079
|
+
if (evidence.has(req)) {
|
|
3080
|
+
const state2 = evidence.get(req);
|
|
3081
|
+
state2.status = "failed";
|
|
3082
|
+
delete req.auth;
|
|
3083
|
+
const error = reentryError();
|
|
3084
|
+
const response = prepareError(error);
|
|
3085
|
+
await notifyMiddlewareError(onError, { error, req, res });
|
|
3086
|
+
delete req.auth;
|
|
3087
|
+
if (res.headersSent) next(error);
|
|
3088
|
+
else sendError(res, response);
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
const state = { owner, res, status: "pending" };
|
|
3092
|
+
const diagnostic = createDiagnostic("api");
|
|
3093
|
+
evidence.set(req, state);
|
|
2581
3094
|
let token;
|
|
2582
3095
|
let scheme;
|
|
2583
3096
|
let nonceResponse;
|
|
3097
|
+
let stage = "credential";
|
|
2584
3098
|
const authenticatedRequest = req;
|
|
2585
3099
|
delete authenticatedRequest.auth;
|
|
2586
3100
|
if (!res.headersSent) res.setHeader("Cache-Control", "no-store");
|
|
2587
|
-
const
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
headers: { "Cache-Control": "no-store" }
|
|
2592
|
-
});
|
|
2593
|
-
const headers = { ...response.headers };
|
|
2594
|
-
if (response.status === 401 && headers["WWW-Authenticate"] === void 0) {
|
|
2595
|
-
const code = response.body.code;
|
|
2596
|
-
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"';
|
|
2597
|
-
headers["WWW-Authenticate"] = challenge2;
|
|
2598
|
-
}
|
|
2599
|
-
return Object.freeze({
|
|
2600
|
-
status: response.status,
|
|
2601
|
-
body: Object.freeze({ ...response.body }),
|
|
2602
|
-
headers: Object.freeze(headers)
|
|
2603
|
-
});
|
|
2604
|
-
};
|
|
2605
|
-
const sendError = (response) => {
|
|
2606
|
-
res.removeHeader("WWW-Authenticate");
|
|
2607
|
-
res.removeHeader("DPoP-Nonce");
|
|
2608
|
-
for (const [name, value] of Object.entries(response.headers)) res.setHeader(name, value);
|
|
2609
|
-
res.status(response.status).json(response.body);
|
|
3101
|
+
const stillPending = () => {
|
|
3102
|
+
if (state.status === "pending") return true;
|
|
3103
|
+
delete authenticatedRequest.auth;
|
|
3104
|
+
return false;
|
|
2610
3105
|
};
|
|
2611
3106
|
try {
|
|
3107
|
+
const authorization = readDpopRawHeader(req, "authorization");
|
|
3108
|
+
const proofHeader = readDpopRawHeader(req, "dpop");
|
|
3109
|
+
if (authentication === "optional" && authorization.count === 0 && proofHeader.count === 0) {
|
|
3110
|
+
diagnostic.emit("presentation", "ignored", policy ? "proof-absent" : "policy-disabled");
|
|
3111
|
+
state.status = "missing";
|
|
3112
|
+
clearChallenge(res);
|
|
3113
|
+
next();
|
|
3114
|
+
return;
|
|
3115
|
+
}
|
|
2612
3116
|
let result;
|
|
2613
3117
|
let confirmation;
|
|
2614
3118
|
let deviceBinding;
|
|
2615
3119
|
if (!policy) {
|
|
3120
|
+
if (authorization.count > 1 || authorization.count === 1 && !authorization.value?.trim())
|
|
3121
|
+
throw new OidcVaultHttpError(
|
|
3122
|
+
401,
|
|
3123
|
+
"OIDC_VAULT_INVALID_AUTHORIZATION_HEADER",
|
|
3124
|
+
"Authorization header must use the Bearer scheme."
|
|
3125
|
+
);
|
|
2616
3126
|
token = extractBearerToken(req.get("authorization"));
|
|
2617
3127
|
scheme = "Bearer";
|
|
3128
|
+
stage = "validator";
|
|
2618
3129
|
result = await validate(token);
|
|
3130
|
+
if (!stillPending()) return;
|
|
2619
3131
|
delete authenticatedRequest.auth;
|
|
3132
|
+
stage = "result";
|
|
2620
3133
|
const reported = result?.confirmation;
|
|
2621
3134
|
confirmation = reported === void 0 ? void 0 : snapshotAccessTokenConfirmation(reported);
|
|
2622
3135
|
result = mappedFields(result);
|
|
3136
|
+
diagnostic.associate(diagnosticCorrelation("verified-token", result.subject, result.sessionId));
|
|
2623
3137
|
delete authenticatedRequest.auth;
|
|
3138
|
+
stage = "proof";
|
|
2624
3139
|
if (confirmation)
|
|
2625
3140
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_DPOP_REQUIRED", "DPoP authentication is required.");
|
|
3141
|
+
diagnostic.emit("presentation", "ignored", "policy-disabled");
|
|
2626
3142
|
} else {
|
|
2627
|
-
const authorization = readDpopRawHeader(req, "authorization");
|
|
2628
3143
|
scheme = schemeOf(authorization.value);
|
|
2629
|
-
if (authorization.count === 0
|
|
3144
|
+
if (authorization.count === 0) {
|
|
2630
3145
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_MISSING_ACCESS_TOKEN", "Missing access token.");
|
|
2631
3146
|
}
|
|
2632
3147
|
const credential = authorization.value?.trim().split(/\s+/);
|
|
@@ -2638,13 +3153,17 @@ function createOidcVaultAccessTokenMiddleware(options) {
|
|
|
2638
3153
|
);
|
|
2639
3154
|
}
|
|
2640
3155
|
token = credential[1];
|
|
2641
|
-
const proofHeader = readDpopRawHeader(req, "dpop");
|
|
2642
3156
|
const { method, originalUrl } = req;
|
|
3157
|
+
stage = "validator";
|
|
2643
3158
|
result = await validateWithRequest(Object.freeze({ token, scheme, req }));
|
|
3159
|
+
if (!stillPending()) return;
|
|
2644
3160
|
delete authenticatedRequest.auth;
|
|
3161
|
+
stage = "result";
|
|
2645
3162
|
confirmation = snapshotAccessTokenConfirmation(result?.confirmation);
|
|
2646
3163
|
result = mappedFields(result);
|
|
3164
|
+
diagnostic.associate(diagnosticCorrelation("verified-token", result.subject, result.sessionId));
|
|
2647
3165
|
delete authenticatedRequest.auth;
|
|
3166
|
+
stage = "proof";
|
|
2648
3167
|
if (confirmation && scheme !== "DPoP")
|
|
2649
3168
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_DPOP_REQUIRED", "DPoP authentication is required.");
|
|
2650
3169
|
if (!confirmation && scheme === "DPoP")
|
|
@@ -2652,7 +3171,7 @@ function createOidcVaultAccessTokenMiddleware(options) {
|
|
|
2652
3171
|
if (!confirmation && policy.mode === "required") {
|
|
2653
3172
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_DEVICE_BINDING_REQUIRED", "A device-bound login is required.");
|
|
2654
3173
|
}
|
|
2655
|
-
const proof = extractDpopProof(proofHeader);
|
|
3174
|
+
const proof = extractDpopProof(proofHeader, diagnostic);
|
|
2656
3175
|
if (!proof && confirmation)
|
|
2657
3176
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_DPOP_REQUIRED", "DPoP authentication is required.");
|
|
2658
3177
|
if (proof) {
|
|
@@ -2663,15 +3182,18 @@ function createOidcVaultAccessTokenMiddleware(options) {
|
|
|
2663
3182
|
targetUrl: () => targetFor(originalUrl),
|
|
2664
3183
|
proofOptions: policy,
|
|
2665
3184
|
accessToken: token,
|
|
2666
|
-
expectedJkt: confirmation?.jkt
|
|
2667
|
-
|
|
3185
|
+
expectedJkt: confirmation?.jkt,
|
|
3186
|
+
diagnostic
|
|
3187
|
+
}),
|
|
3188
|
+
diagnostic
|
|
2668
3189
|
);
|
|
3190
|
+
if (!stillPending()) return;
|
|
2669
3191
|
if (admission.type === "nonce-challenge") {
|
|
2670
3192
|
nonceResponse = toDpopNonceChallengeResponse(admission, "api", policy.algorithms);
|
|
2671
3193
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_USE_DPOP_NONCE", "A fresh DPoP nonce is required.");
|
|
2672
3194
|
}
|
|
2673
3195
|
if (confirmation) deviceBinding = admission.binding;
|
|
2674
|
-
}
|
|
3196
|
+
} else diagnostic.emit("presentation", "ignored", "proof-absent");
|
|
2675
3197
|
}
|
|
2676
3198
|
const authenticatedToken = token;
|
|
2677
3199
|
const auth = {
|
|
@@ -2695,9 +3217,12 @@ function createOidcVaultAccessTokenMiddleware(options) {
|
|
|
2695
3217
|
authenticatedRequest.auth = auth;
|
|
2696
3218
|
try {
|
|
2697
3219
|
await onAuthContext?.({ req: authenticatedRequest, res, auth });
|
|
3220
|
+
if (!stillPending()) return;
|
|
2698
3221
|
authenticatedRequest.auth = restoreAuth();
|
|
2699
3222
|
} catch (hookError) {
|
|
2700
|
-
|
|
3223
|
+
if (!stillPending()) return;
|
|
3224
|
+
state.status = "failed";
|
|
3225
|
+
const response = prepareError(hookError, scheme, void 0, true);
|
|
2701
3226
|
delete authenticatedRequest.auth;
|
|
2702
3227
|
let errorAuth;
|
|
2703
3228
|
try {
|
|
@@ -2716,13 +3241,25 @@ function createOidcVaultAccessTokenMiddleware(options) {
|
|
|
2716
3241
|
next(hookError);
|
|
2717
3242
|
return;
|
|
2718
3243
|
}
|
|
2719
|
-
sendError(response);
|
|
3244
|
+
sendError(res, response);
|
|
2720
3245
|
return;
|
|
2721
3246
|
}
|
|
3247
|
+
state.auth = authenticatedRequest.auth;
|
|
3248
|
+
state.snapshot = { ...authenticatedRequest.auth };
|
|
3249
|
+
state.status = "authenticated";
|
|
2722
3250
|
if (!res.headersSent) res.setHeader("Cache-Control", "no-store");
|
|
2723
3251
|
next();
|
|
2724
3252
|
} catch (error) {
|
|
2725
|
-
|
|
3253
|
+
if (!stillPending()) return;
|
|
3254
|
+
diagnostic.emit(
|
|
3255
|
+
"preflight",
|
|
3256
|
+
"rejected",
|
|
3257
|
+
stage === "proof" ? diagnosticPreflightReason(error instanceof OidcVaultHttpError ? error.code : void 0) : "credential-rejected"
|
|
3258
|
+
);
|
|
3259
|
+
const response = prepareError(error, scheme, nonceResponse);
|
|
3260
|
+
const rejected = stage === "validator" ? isAccessTokenRejected(error) : (stage === "credential" || stage === "proof") && error instanceof OidcVaultHttpError && credentialRejectionCodes.has(error.code);
|
|
3261
|
+
const anonymous = authentication === "optional" && invalidCredentials === "anonymous" && rejected && !nonceResponse;
|
|
3262
|
+
state.status = anonymous ? "invalid" : nonceResponse ? "challenged" : "failed";
|
|
2726
3263
|
delete authenticatedRequest.auth;
|
|
2727
3264
|
await notifyMiddlewareError(onError, { error, req, res, ...token ? { token } : {} });
|
|
2728
3265
|
delete authenticatedRequest.auth;
|
|
@@ -2730,9 +3267,139 @@ function createOidcVaultAccessTokenMiddleware(options) {
|
|
|
2730
3267
|
next(error);
|
|
2731
3268
|
return;
|
|
2732
3269
|
}
|
|
2733
|
-
|
|
3270
|
+
if (anonymous && state.status === "invalid") {
|
|
3271
|
+
clearChallenge(res);
|
|
3272
|
+
res.status(200);
|
|
3273
|
+
next();
|
|
3274
|
+
} else sendError(res, response);
|
|
2734
3275
|
}
|
|
2735
3276
|
};
|
|
3277
|
+
return Object.freeze({ authenticate, requireAuth: guard(false), requireDeviceBinding: guard(true) });
|
|
3278
|
+
}
|
|
3279
|
+
|
|
3280
|
+
// src/jwt-token-issuer.ts
|
|
3281
|
+
var import_node_crypto8 = require("crypto");
|
|
3282
|
+
var import_node_util = require("util");
|
|
3283
|
+
var import_jose5 = require("jose");
|
|
3284
|
+
var ALGORITHMS = /* @__PURE__ */ new Set([
|
|
3285
|
+
"HS256",
|
|
3286
|
+
"HS384",
|
|
3287
|
+
"HS512",
|
|
3288
|
+
"RS256",
|
|
3289
|
+
"RS384",
|
|
3290
|
+
"RS512",
|
|
3291
|
+
"PS256",
|
|
3292
|
+
"PS384",
|
|
3293
|
+
"PS512",
|
|
3294
|
+
"ES256",
|
|
3295
|
+
"ES384",
|
|
3296
|
+
"ES512",
|
|
3297
|
+
"EdDSA"
|
|
3298
|
+
]);
|
|
3299
|
+
var RESERVED = ["iss", "aud", "iat", "exp", "sub", "cnf"];
|
|
3300
|
+
var CURVES = { ES256: "prime256v1", ES384: "secp384r1", ES512: "secp521r1" };
|
|
3301
|
+
var requireString = (value, name) => {
|
|
3302
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new TypeError(`${name} must be a nonblank string.`);
|
|
3303
|
+
return value;
|
|
3304
|
+
};
|
|
3305
|
+
var signingKey = (configured, algorithm) => {
|
|
3306
|
+
const symmetric = algorithm.startsWith("HS");
|
|
3307
|
+
let key;
|
|
3308
|
+
let cryptoKey;
|
|
3309
|
+
if (configured instanceof Uint8Array) {
|
|
3310
|
+
key = (0, import_node_crypto8.createSecretKey)(Uint8Array.from(configured));
|
|
3311
|
+
} else if (configured instanceof import_node_crypto8.KeyObject) {
|
|
3312
|
+
key = configured;
|
|
3313
|
+
} else if (import_node_util.types.isCryptoKey(configured)) {
|
|
3314
|
+
cryptoKey = configured;
|
|
3315
|
+
const expectedName = symmetric ? "HMAC" : algorithm.startsWith("RS") ? "RSASSA-PKCS1-v1_5" : algorithm.startsWith("PS") ? "RSA-PSS" : algorithm.startsWith("ES") ? "ECDSA" : "Ed25519";
|
|
3316
|
+
const parameters = configured.algorithm;
|
|
3317
|
+
if (!configured.usages.includes("sign") || parameters.name !== expectedName || (symmetric || algorithm.startsWith("RS") || algorithm.startsWith("PS")) && parameters.hash?.name !== `SHA-${algorithm.slice(2)}`)
|
|
3318
|
+
throw new TypeError("JWT signing CryptoKey algorithm/hash/usages must match the signing algorithm.");
|
|
3319
|
+
key = import_node_crypto8.KeyObject.from(configured);
|
|
3320
|
+
} else {
|
|
3321
|
+
if (!isRecord(configured)) throw new TypeError("JWT signing key must be a JOSE key.");
|
|
3322
|
+
const jwk = { ...configured, ...configured.key_ops ? { key_ops: [...configured.key_ops] } : {} };
|
|
3323
|
+
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")))
|
|
3324
|
+
throw new TypeError("JWT signing JWK alg/use/key_ops must permit the signing algorithm.");
|
|
3325
|
+
if (jwk.kty === "oct") {
|
|
3326
|
+
if (typeof jwk.k !== "string" || !/^[A-Za-z0-9_-]+$/.test(jwk.k) || Buffer.from(jwk.k, "base64url").toString("base64url") !== jwk.k)
|
|
3327
|
+
throw new TypeError("JWT signing JWK k must be canonical base64url secret bytes.");
|
|
3328
|
+
key = (0, import_node_crypto8.createSecretKey)(Buffer.from(jwk.k, "base64url"));
|
|
3329
|
+
} else {
|
|
3330
|
+
key = (0, import_node_crypto8.createPrivateKey)({ key: jwk, format: "jwk" });
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
const bits = Number(algorithm.slice(2));
|
|
3334
|
+
const curve = CURVES[algorithm];
|
|
3335
|
+
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);
|
|
3336
|
+
if (!compatible) throw new TypeError("JWT signing key type/size/curve must match the signing algorithm.");
|
|
3337
|
+
return cryptoKey ?? key;
|
|
3338
|
+
};
|
|
3339
|
+
var tokenTimes = (now, lifetimeSeconds) => {
|
|
3340
|
+
const milliseconds = now();
|
|
3341
|
+
const lifetimeMs = lifetimeSeconds * 1e3;
|
|
3342
|
+
const iat = Math.floor(milliseconds / 1e3);
|
|
3343
|
+
const exp = iat + lifetimeSeconds;
|
|
3344
|
+
if (!Number.isSafeInteger(milliseconds) || milliseconds < 0 || !Number.isSafeInteger(lifetimeMs) || !Number.isSafeInteger(milliseconds + lifetimeMs) || !Number.isSafeInteger(exp) || !Number.isSafeInteger(exp * 1e3))
|
|
3345
|
+
throw new TypeError("JWT clock/lifetime must produce safe nonnegative integer milliseconds and expiry.");
|
|
3346
|
+
return { iat, exp };
|
|
3347
|
+
};
|
|
3348
|
+
function assertApplicationClaims(claims) {
|
|
3349
|
+
if (!isRecord(claims) || Array.isArray(claims)) throw new TypeError("JWT mapClaims must return a claims object.");
|
|
3350
|
+
for (const field of RESERVED) {
|
|
3351
|
+
if (field in claims) throw new TypeError(`JWT mapClaims must not supply reserved claim ${field}.`);
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
function createOidcVaultJwtAccessTokenIssuer(options) {
|
|
3355
|
+
const {
|
|
3356
|
+
algorithm,
|
|
3357
|
+
issuer: configuredIssuer,
|
|
3358
|
+
audience: configuredAudience,
|
|
3359
|
+
lifetimeSeconds = 300,
|
|
3360
|
+
now = Date.now,
|
|
3361
|
+
mapSubject,
|
|
3362
|
+
mapClaims
|
|
3363
|
+
} = options;
|
|
3364
|
+
if (!ALGORITHMS.has(algorithm)) throw new TypeError("JWT signing algorithm is unsupported.");
|
|
3365
|
+
const issuer = requireString(configuredIssuer, "JWT issuer");
|
|
3366
|
+
const audience = Array.isArray(configuredAudience) ? [...configuredAudience] : configuredAudience;
|
|
3367
|
+
if (Array.isArray(audience)) {
|
|
3368
|
+
if (audience.length === 0) throw new TypeError("JWT audience must not be empty.");
|
|
3369
|
+
for (const value of audience) requireString(value, "JWT audience");
|
|
3370
|
+
} else requireString(audience, "JWT audience");
|
|
3371
|
+
if (!Number.isSafeInteger(lifetimeSeconds) || lifetimeSeconds < 0)
|
|
3372
|
+
throw new TypeError("JWT lifetimeSeconds must be a nonnegative safe integer.");
|
|
3373
|
+
if (typeof now !== "function" || mapSubject !== void 0 && typeof mapSubject !== "function" || mapClaims !== void 0 && typeof mapClaims !== "function")
|
|
3374
|
+
throw new TypeError("JWT clock and mappers must be functions.");
|
|
3375
|
+
const key = signingKey(options.key, algorithm);
|
|
3376
|
+
tokenTimes(now, lifetimeSeconds);
|
|
3377
|
+
return Object.freeze({
|
|
3378
|
+
async issue(input) {
|
|
3379
|
+
const { deviceBinding, session } = input;
|
|
3380
|
+
const { subject: defaultSubject, scope } = session;
|
|
3381
|
+
if (scope !== void 0 && typeof scope !== "string") throw new TypeError("JWT session scope must be a string.");
|
|
3382
|
+
if (deviceBinding !== void 0 && deviceBinding.type !== "dpop")
|
|
3383
|
+
throw new TypeError("JWT deviceBinding must be verified DPoP context.");
|
|
3384
|
+
const cnf = deviceBinding === void 0 ? void 0 : snapshotJkt({ jkt: deviceBinding.jkt }, (message) => new TypeError(message));
|
|
3385
|
+
const subject = requireString(mapSubject ? await mapSubject(input) : defaultSubject, "JWT subject");
|
|
3386
|
+
const mapped = mapClaims ? await mapClaims(input) : {};
|
|
3387
|
+
assertApplicationClaims(mapped);
|
|
3388
|
+
const claims = JSON.parse(JSON.stringify({ ...scope === void 0 ? {} : { scope }, ...mapped }));
|
|
3389
|
+
assertApplicationClaims(claims);
|
|
3390
|
+
const { iat, exp } = tokenTimes(now, lifetimeSeconds);
|
|
3391
|
+
const accessToken = await new import_jose5.SignJWT({
|
|
3392
|
+
...claims,
|
|
3393
|
+
iss: issuer,
|
|
3394
|
+
aud: audience,
|
|
3395
|
+
sub: subject,
|
|
3396
|
+
iat,
|
|
3397
|
+
exp,
|
|
3398
|
+
...cnf === void 0 ? {} : { cnf }
|
|
3399
|
+
}).setProtectedHeader({ alg: algorithm, typ: "JWT" }).sign(key);
|
|
3400
|
+
return { accessToken, expiresIn: lifetimeSeconds, tokenType: cnf === void 0 ? "Bearer" : "DPoP" };
|
|
3401
|
+
}
|
|
3402
|
+
});
|
|
2736
3403
|
}
|
|
2737
3404
|
|
|
2738
3405
|
// src/index.ts
|
|
@@ -2747,24 +3414,30 @@ var CREDENTIAL_RESPONSE_CACHE_CONTROL = "no-store";
|
|
|
2747
3414
|
var applyCredentialResponseCachePolicy = (res) => {
|
|
2748
3415
|
res.setHeader("Cache-Control", CREDENTIAL_RESPONSE_CACHE_CONTROL);
|
|
2749
3416
|
};
|
|
2750
|
-
var createOpaqueId = (prefix) => `${prefix}_${(0,
|
|
2751
|
-
var createPkceVerifier = () => (0,
|
|
2752
|
-
var createPkceChallenge = (verifier) => (0,
|
|
3417
|
+
var createOpaqueId = (prefix) => `${prefix}_${(0, import_node_crypto9.randomBytes)(16).toString("base64url")}`;
|
|
3418
|
+
var createPkceVerifier = () => (0, import_node_crypto9.randomBytes)(32).toString("base64url");
|
|
3419
|
+
var createPkceChallenge = (verifier) => (0, import_node_crypto9.createHash)("sha256").update(verifier).digest("base64url");
|
|
2753
3420
|
var getCallbackUri = (backendOrigin, basePath) => `${backendOrigin}${basePath}${OIDC_VAULT_ROUTE_PATHS.callback}`;
|
|
2754
3421
|
var validateOidcVaultOptions = (options) => {
|
|
2755
3422
|
const {
|
|
2756
3423
|
deviceBinding: configuredDeviceBinding,
|
|
2757
3424
|
fingerprintRecognition: configuredFingerprintRecognition,
|
|
2758
3425
|
transactionCookie: configuredTransactionCookie,
|
|
3426
|
+
logoutNavigation: configuredLogoutNavigation,
|
|
2759
3427
|
...otherOptions
|
|
2760
3428
|
} = options;
|
|
2761
3429
|
const cookie = otherOptions.cookie ? { ...otherOptions.cookie } : void 0;
|
|
2762
3430
|
const cookieOptions = { ...otherOptions, cookie };
|
|
2763
|
-
|
|
3431
|
+
const now = getNow(options);
|
|
3432
|
+
validateLifetimeOptions(options, now);
|
|
2764
3433
|
const deviceBinding = resolveDeviceBindingOptions(configuredDeviceBinding);
|
|
2765
3434
|
const fingerprintRecognition = resolveFingerprintRecognitionOptions(configuredFingerprintRecognition);
|
|
2766
3435
|
const backendOrigin = deviceBinding === void 0 ? resolveBackendOrigin(options) : resolveDeviceBindingOrigin(options.backendOrigin);
|
|
2767
|
-
if (deviceBinding !== void 0)
|
|
3436
|
+
if (deviceBinding !== void 0)
|
|
3437
|
+
assertDeviceBindingStoreCapabilities(
|
|
3438
|
+
options.storeProvider,
|
|
3439
|
+
configuredLogoutNavigation === void 0 ? "deviceBinding" : "logoutNavigation"
|
|
3440
|
+
);
|
|
2768
3441
|
if (fingerprintRecognition !== void 0)
|
|
2769
3442
|
assertDeviceBindingStoreCapabilities(options.storeProvider, "fingerprintRecognition");
|
|
2770
3443
|
const frontendRedirectUri = resolveFrontendRedirectUri(options);
|
|
@@ -2780,6 +3453,16 @@ var validateOidcVaultOptions = (options) => {
|
|
|
2780
3453
|
backendOrigin
|
|
2781
3454
|
);
|
|
2782
3455
|
const config = resolveOidcVaultConfig(options.config ? { ...options.config } : void 0);
|
|
3456
|
+
const logoutNavigation = resolveLogoutNavigationOptions(configuredLogoutNavigation, {
|
|
3457
|
+
backendOrigin,
|
|
3458
|
+
basePath: normalizeOidcVaultBasePath(otherOptions.basePath),
|
|
3459
|
+
config,
|
|
3460
|
+
frontendRedirectUri,
|
|
3461
|
+
postLogoutRedirectUri: otherOptions.postLogoutRedirectUri,
|
|
3462
|
+
deviceBindingConfigured: deviceBinding !== void 0,
|
|
3463
|
+
store: otherOptions.storeProvider,
|
|
3464
|
+
now
|
|
3465
|
+
});
|
|
2783
3466
|
const configuredTrustedOrigins = resolveTrustedOrigins(options);
|
|
2784
3467
|
if (usesCrossSiteCookieTransport(cookieOptions) && configuredTrustedOrigins.size === 0) {
|
|
2785
3468
|
throw new Error("trustedOrigins is required when using cross-site cookie transport.");
|
|
@@ -2791,6 +3474,8 @@ var validateOidcVaultOptions = (options) => {
|
|
|
2791
3474
|
deviceBinding,
|
|
2792
3475
|
fingerprintRecognition,
|
|
2793
3476
|
transactionCookie,
|
|
3477
|
+
logoutNavigation,
|
|
3478
|
+
...logoutNavigation === void 0 ? {} : { postLogoutRedirectUri: logoutNavigation.postLogoutRedirectUri },
|
|
2794
3479
|
...frontendRedirectUri !== void 0 ? { frontendRedirectUri } : {},
|
|
2795
3480
|
...options.trustedOrigins ? { trustedOrigins: [...options.trustedOrigins] } : {},
|
|
2796
3481
|
...cookie === void 0 ? {} : { cookie },
|
|
@@ -2938,10 +3623,18 @@ async function handleRouteError(route, req, res, next, options, error) {
|
|
|
2938
3623
|
sendVaultRoutePolicyResponse(res, response);
|
|
2939
3624
|
}
|
|
2940
3625
|
function createAsyncHandler(route, options, handler) {
|
|
3626
|
+
const createDiagnostic = createDpopDiagnosticFactory(options.onDpopDiagnostic);
|
|
3627
|
+
const notify = createDiagnosticDispatcher(options.onTokenOutcome);
|
|
2941
3628
|
return async (req, res, next) => {
|
|
3629
|
+
const diagnostic = req.method === "POST" && route !== "callback" && route !== "backchannel-logout" ? createDiagnostic(route) : void 0;
|
|
2942
3630
|
try {
|
|
2943
|
-
await handler(req, res, next);
|
|
3631
|
+
await handler(req, res, next, diagnostic, notify);
|
|
2944
3632
|
} catch (error) {
|
|
3633
|
+
diagnostic?.emit(
|
|
3634
|
+
"preflight",
|
|
3635
|
+
"rejected",
|
|
3636
|
+
diagnosticPreflightReason(error instanceof OidcVaultHttpError ? error.code : void 0)
|
|
3637
|
+
);
|
|
2945
3638
|
await handleRouteError(route, req, res, next, options, error);
|
|
2946
3639
|
}
|
|
2947
3640
|
};
|
|
@@ -2987,8 +3680,8 @@ var createLoginHandler = (options, config, backendOrigin, basePath) => createAsy
|
|
|
2987
3680
|
});
|
|
2988
3681
|
res.redirect(302, authorizationUrl);
|
|
2989
3682
|
});
|
|
2990
|
-
var createLoginInitiationHandler = (options, config, trustedOrigins, backendOrigin, basePath, proofVerifier) => createAsyncHandler("login", options, async (req, res) => {
|
|
2991
|
-
const capturedProof = captureVaultRouteProof(req);
|
|
3683
|
+
var createLoginInitiationHandler = (options, config, trustedOrigins, backendOrigin, basePath, proofVerifier) => createAsyncHandler("login", options, async (req, res, _next, diagnostic) => {
|
|
3684
|
+
const capturedProof = captureVaultRouteProof(req, diagnostic);
|
|
2992
3685
|
const fingerprintRecognition = captureFingerprintRecognition(req, options.fingerprintRecognition);
|
|
2993
3686
|
assertLoginInitiationRequest(req, options, trustedOrigins);
|
|
2994
3687
|
const body = getBody(req);
|
|
@@ -2997,6 +3690,7 @@ var createLoginInitiationHandler = (options, config, trustedOrigins, backendOrig
|
|
|
2997
3690
|
}
|
|
2998
3691
|
const returnTo = resolveLoginReturnTo(body.returnTo, options);
|
|
2999
3692
|
if (proofVerifier === void 0 && capturedProof.header.count !== 0) {
|
|
3693
|
+
diagnostic?.emit("presentation", "rejected", "policy-disabled");
|
|
3000
3694
|
throw new OidcVaultHttpError(
|
|
3001
3695
|
401,
|
|
3002
3696
|
"OIDC_VAULT_INVALID_DPOP_PROOF",
|
|
@@ -3005,6 +3699,7 @@ var createLoginInitiationHandler = (options, config, trustedOrigins, backendOrig
|
|
|
3005
3699
|
);
|
|
3006
3700
|
}
|
|
3007
3701
|
const verifiedBinding = await proofVerifier?.initiate(capturedProof);
|
|
3702
|
+
if (proofVerifier === void 0) diagnostic?.emit("presentation", "ignored", "policy-disabled");
|
|
3008
3703
|
const binding = snapshotDpopBinding(verifiedBinding);
|
|
3009
3704
|
const metadata = await resolveProviderMetadata(config, options);
|
|
3010
3705
|
await callHook("login", options.hooks?.onLoginStart, req, res, void 0, { provider: { ...metadata } });
|
|
@@ -3222,8 +3917,8 @@ var copySessionApplicationFields = (session) => {
|
|
|
3222
3917
|
}
|
|
3223
3918
|
return result;
|
|
3224
3919
|
};
|
|
3225
|
-
var createExchangeHandler = (options, config, trustedOrigins, proofVerifier) => createAsyncHandler("exchange", options, async (req, res) => {
|
|
3226
|
-
const capturedProof = proofVerifier === void 0 ? void 0 : captureVaultRouteProof(req);
|
|
3920
|
+
var createExchangeHandler = (options, config, trustedOrigins, proofVerifier) => createAsyncHandler("exchange", options, async (req, res, _next, diagnostic, notify) => {
|
|
3921
|
+
const capturedProof = proofVerifier === void 0 ? void 0 : captureVaultRouteProof(req, diagnostic);
|
|
3227
3922
|
const capturedRecognition = captureFingerprintRecognition(req, options.fingerprintRecognition);
|
|
3228
3923
|
const assertExchangeOrigin = captureTrustedOriginGuard(req, options, trustedOrigins, "exchange");
|
|
3229
3924
|
const cookieHeader = req.headers.cookie;
|
|
@@ -3235,14 +3930,24 @@ var createExchangeHandler = (options, config, trustedOrigins, proofVerifier) =>
|
|
|
3235
3930
|
throw new TypeError("Device-bound exchange requires guarded store reads and atomic consumption.");
|
|
3236
3931
|
}
|
|
3237
3932
|
const rawCode = canPreflight ? await store.getExchangeCode(code) : await store.consumeExchangeCode(code);
|
|
3238
|
-
if (!rawCode)
|
|
3933
|
+
if (!rawCode) {
|
|
3934
|
+
diagnostic?.emit("preflight", "rejected", "session-unavailable");
|
|
3935
|
+
throw invalidExchangeCode();
|
|
3936
|
+
}
|
|
3239
3937
|
const record = snapshotVaultExchangeCode(rawCode, code, getNow(options));
|
|
3938
|
+
diagnostic?.associate(diagnosticCorrelation("candidate-session", void 0, record.sessionId));
|
|
3240
3939
|
const guarded = record.browserBindingHash !== void 0;
|
|
3241
3940
|
if (guarded) assertExchangeOrigin();
|
|
3242
3941
|
assertDeviceBindingPolicy(options.deviceBinding, record.deviceBinding);
|
|
3243
3942
|
const rawSession = await store.getSession(record.sessionId);
|
|
3244
|
-
if (!rawSession)
|
|
3943
|
+
if (!rawSession) {
|
|
3944
|
+
diagnostic?.emit("preflight", "rejected", "session-unavailable");
|
|
3945
|
+
throw invalidVaultSession();
|
|
3946
|
+
}
|
|
3245
3947
|
const session = snapshotVaultSession(rawSession, record.sessionId, getNow(options));
|
|
3948
|
+
diagnostic?.associate(
|
|
3949
|
+
diagnosticCorrelation("candidate-session", session.subject, session.logicalSessionId ?? session.sessionId)
|
|
3950
|
+
);
|
|
3246
3951
|
assertDeviceBindingPolicy(options.deviceBinding, session.deviceBinding);
|
|
3247
3952
|
assertVaultExchangeSessionBinding(record, session);
|
|
3248
3953
|
const protectedExchange = proofVerifier !== void 0 || options.fingerprintRecognition !== void 0 || guarded;
|
|
@@ -3254,6 +3959,7 @@ var createExchangeHandler = (options, config, trustedOrigins, proofVerifier) =>
|
|
|
3254
3959
|
const recognition = options.fingerprintRecognition === void 0 ? void 0 : snapshotFingerprintRecognition(session.metadata);
|
|
3255
3960
|
assertFingerprintRecognition(options.fingerprintRecognition, recognition, capturedRecognition);
|
|
3256
3961
|
const verifiedBinding = proofVerifier === void 0 ? void 0 : await proofVerifier.verify(capturedProof, session.deviceBinding);
|
|
3962
|
+
if (proofVerifier === void 0) diagnostic?.emit("presentation", "ignored", "policy-disabled");
|
|
3257
3963
|
if (canPreflight) {
|
|
3258
3964
|
const consumed = protectedExchange ? await store.consumeExchangeCodeIfMatches({
|
|
3259
3965
|
code,
|
|
@@ -3274,34 +3980,45 @@ var createExchangeHandler = (options, config, trustedOrigins, proofVerifier) =>
|
|
|
3274
3980
|
}
|
|
3275
3981
|
assertVaultSessionIdentity(session, config);
|
|
3276
3982
|
try {
|
|
3277
|
-
const issuedToken = await withIssuedToken(req, res, options, session, verifiedBinding);
|
|
3983
|
+
const issuedToken = await withIssuedToken(req, res, options, session, "exchange", verifiedBinding);
|
|
3278
3984
|
if (usesCookieTransport(options)) setSessionCookie(res, options, session.sessionId);
|
|
3279
3985
|
if (cookieSecret !== void 0) clearTransactionCookie(res, options.transactionCookie);
|
|
3280
3986
|
applyCredentialResponseCachePolicy(res);
|
|
3281
|
-
|
|
3987
|
+
const response = createExchangeResponse(options, session, issuedToken);
|
|
3988
|
+
notifyTokenOutcomes("exchange", session, issuedToken, notify, diagnostic);
|
|
3989
|
+
res.status(200).json(response);
|
|
3282
3990
|
} catch (error) {
|
|
3283
3991
|
if (cookieSecret !== void 0) clearTransactionCookie(res, options.transactionCookie);
|
|
3284
3992
|
throw error;
|
|
3285
3993
|
}
|
|
3286
3994
|
});
|
|
3287
|
-
var createRefreshHandler = (options, config, trustedOrigins, proofVerifier) => createAsyncHandler("refresh", options, async (req, res) => {
|
|
3995
|
+
var createRefreshHandler = (options, config, trustedOrigins, proofVerifier) => createAsyncHandler("refresh", options, async (req, res, _next, diagnostic, notify) => {
|
|
3288
3996
|
const capturedRecognition = captureFingerprintRecognition(req, options.fingerprintRecognition);
|
|
3289
3997
|
assertTrustedOrigin(req, options, trustedOrigins, "refresh");
|
|
3290
|
-
const capturedProof = proofVerifier === void 0 ? void 0 : captureVaultRouteProof(req);
|
|
3998
|
+
const capturedProof = proofVerifier === void 0 ? void 0 : captureVaultRouteProof(req, diagnostic);
|
|
3291
3999
|
const sessionId = getSessionIdFromRequest(req, options, "refresh");
|
|
3292
4000
|
const currentRecord = await options.storeProvider.getSession(sessionId);
|
|
3293
4001
|
if (!currentRecord) {
|
|
4002
|
+
diagnostic?.emit("preflight", "rejected", "session-unavailable");
|
|
3294
4003
|
if (usesCookieTransport(options)) {
|
|
3295
4004
|
clearSessionCookie(res, options);
|
|
3296
4005
|
}
|
|
3297
4006
|
throw new OidcVaultHttpError(401, "OIDC_VAULT_INVALID_SESSION", "Session is missing or expired.");
|
|
3298
4007
|
}
|
|
3299
4008
|
const currentSession = snapshotVaultSession(currentRecord, sessionId, getNow(options));
|
|
4009
|
+
diagnostic?.associate(
|
|
4010
|
+
diagnosticCorrelation(
|
|
4011
|
+
"candidate-session",
|
|
4012
|
+
currentSession.subject,
|
|
4013
|
+
currentSession.logicalSessionId ?? currentSession.sessionId
|
|
4014
|
+
)
|
|
4015
|
+
);
|
|
3300
4016
|
assertVaultSessionIdentity(currentSession, config);
|
|
3301
4017
|
assertDeviceBindingPolicy(options.deviceBinding, currentSession.deviceBinding);
|
|
3302
4018
|
const recognition = options.fingerprintRecognition === void 0 ? void 0 : snapshotFingerprintRecognition(currentSession.metadata);
|
|
3303
4019
|
assertFingerprintRecognition(options.fingerprintRecognition, recognition, capturedRecognition);
|
|
3304
4020
|
const verifiedBinding = proofVerifier === void 0 ? void 0 : await proofVerifier.verify(capturedProof, currentSession.deviceBinding);
|
|
4021
|
+
if (proofVerifier === void 0) diagnostic?.emit("presentation", "ignored", "policy-disabled");
|
|
3305
4022
|
const metadata = await resolveProviderMetadata(config, options);
|
|
3306
4023
|
if (proofVerifier !== void 0 || options.fingerprintRecognition !== void 0) {
|
|
3307
4024
|
const latest = await options.storeProvider.getSession(sessionId);
|
|
@@ -3375,7 +4092,7 @@ var createRefreshHandler = (options, config, trustedOrigins, proofVerifier) => c
|
|
|
3375
4092
|
throw error;
|
|
3376
4093
|
}
|
|
3377
4094
|
const rotatedSession = expectedSession;
|
|
3378
|
-
const issuedToken = await withIssuedToken(req, res, options, rotatedSession, verifiedBinding);
|
|
4095
|
+
const issuedToken = await withIssuedToken(req, res, options, rotatedSession, "refresh", verifiedBinding);
|
|
3379
4096
|
await callPostCommitHook(
|
|
3380
4097
|
"refresh",
|
|
3381
4098
|
options,
|
|
@@ -3392,14 +4109,21 @@ var createRefreshHandler = (options, config, trustedOrigins, proofVerifier) => c
|
|
|
3392
4109
|
}
|
|
3393
4110
|
const response = createExchangeResponse(options, rotatedSession, issuedToken);
|
|
3394
4111
|
applyCredentialResponseCachePolicy(res);
|
|
4112
|
+
notifyTokenOutcomes("refresh", rotatedSession, issuedToken, notify, diagnostic);
|
|
3395
4113
|
res.status(200).json(response);
|
|
3396
4114
|
});
|
|
3397
|
-
var
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
4115
|
+
var notifyLogoutError = async (options, req, res, error, session) => {
|
|
4116
|
+
try {
|
|
4117
|
+
await options.hooks?.onError?.({
|
|
4118
|
+
...createHookContext("logout", req, res, session === void 0 ? void 0 : copyVaultSession(session)),
|
|
4119
|
+
error
|
|
4120
|
+
});
|
|
4121
|
+
} catch {
|
|
4122
|
+
}
|
|
4123
|
+
};
|
|
4124
|
+
var performLogout = async (options, config, proofVerifier, req, res, diagnostic, redirect, navigation) => {
|
|
4125
|
+
const capturedProof = proofVerifier === void 0 ? void 0 : captureVaultRouteProof(req, diagnostic);
|
|
3401
4126
|
const sessionId = getSessionIdFromRequest(req, options, "logout");
|
|
3402
|
-
const redirect = body.redirect === true;
|
|
3403
4127
|
const store = options.storeProvider;
|
|
3404
4128
|
const hasContext = typeof store.getSessionRevocationContext === "function";
|
|
3405
4129
|
if (proofVerifier !== void 0 && !hasContext) {
|
|
@@ -3409,18 +4133,29 @@ var createLogoutHandler = (options, config, trustedOrigins, proofVerifier) => cr
|
|
|
3409
4133
|
const context = rawContext ? snapshotVaultRevocationContext(rawContext) : void 0;
|
|
3410
4134
|
const rawSession = await store.getSession(sessionId);
|
|
3411
4135
|
const session = rawSession ? snapshotVaultSession(rawSession, sessionId, getNow(options)) : void 0;
|
|
4136
|
+
if (session || context)
|
|
4137
|
+
diagnostic?.associate(
|
|
4138
|
+
diagnosticCorrelation(
|
|
4139
|
+
"candidate-session",
|
|
4140
|
+
session?.subject,
|
|
4141
|
+
context?.logicalSessionId ?? session?.logicalSessionId ?? session?.sessionId
|
|
4142
|
+
)
|
|
4143
|
+
);
|
|
3412
4144
|
const finishLocalLogout = () => {
|
|
4145
|
+
diagnostic?.emit(
|
|
4146
|
+
"presentation",
|
|
4147
|
+
"ignored",
|
|
4148
|
+
options.deviceBinding === void 0 ? "policy-disabled" : "target-absent"
|
|
4149
|
+
);
|
|
3413
4150
|
if (usesCookieTransport(options)) clearSessionCookie(res, options);
|
|
3414
|
-
|
|
3415
|
-
res.status(200).json({ loggedOut: true });
|
|
4151
|
+
return {};
|
|
3416
4152
|
};
|
|
3417
4153
|
if (hasContext && context === void 0) {
|
|
3418
4154
|
if (session !== void 0) {
|
|
3419
4155
|
assertDeviceBindingPolicy(options.deviceBinding, session.deviceBinding);
|
|
3420
4156
|
throw invalidVaultSession();
|
|
3421
4157
|
}
|
|
3422
|
-
finishLocalLogout();
|
|
3423
|
-
return;
|
|
4158
|
+
return finishLocalLogout();
|
|
3424
4159
|
}
|
|
3425
4160
|
const authority = context ?? (session === void 0 ? void 0 : snapshotVaultRevocationContext({
|
|
3426
4161
|
logicalSessionId: session.logicalSessionId ?? session.sessionId,
|
|
@@ -3429,11 +4164,12 @@ var createLogoutHandler = (options, config, trustedOrigins, proofVerifier) => cr
|
|
|
3429
4164
|
}));
|
|
3430
4165
|
if (authority === void 0) {
|
|
3431
4166
|
await store.deleteSession(sessionId);
|
|
3432
|
-
finishLocalLogout();
|
|
3433
|
-
return;
|
|
4167
|
+
return finishLocalLogout();
|
|
3434
4168
|
}
|
|
3435
4169
|
assertVaultSessionIdentity(authority, config);
|
|
3436
4170
|
assertDeviceBindingPolicy(options.deviceBinding, authority.deviceBinding);
|
|
4171
|
+
if (navigation !== void 0 && authority.deviceBinding === void 0)
|
|
4172
|
+
throw new OidcVaultHttpError(401, "OIDC_VAULT_DEVICE_BINDING_REQUIRED", "A device-bound login is required.");
|
|
3437
4173
|
if (session !== void 0)
|
|
3438
4174
|
assertVaultRevocationContextMatches(
|
|
3439
4175
|
snapshotVaultRevocationContext({
|
|
@@ -3444,54 +4180,95 @@ var createLogoutHandler = (options, config, trustedOrigins, proofVerifier) => cr
|
|
|
3444
4180
|
authority
|
|
3445
4181
|
);
|
|
3446
4182
|
if (proofVerifier !== void 0) await proofVerifier.verify(capturedProof, authority.deviceBinding);
|
|
4183
|
+
else diagnostic?.emit("presentation", "ignored", "policy-disabled");
|
|
3447
4184
|
if (hasContext) {
|
|
3448
4185
|
const latest = await store.getSessionRevocationContext(sessionId);
|
|
3449
4186
|
if (!latest) {
|
|
3450
|
-
finishLocalLogout();
|
|
3451
|
-
return;
|
|
4187
|
+
return finishLocalLogout();
|
|
3452
4188
|
}
|
|
3453
4189
|
assertVaultRevocationContextMatches(snapshotVaultRevocationContext(latest), authority);
|
|
3454
4190
|
}
|
|
3455
4191
|
if (session !== void 0)
|
|
3456
4192
|
await callHook("logout", options.hooks?.onBeforeLogout, req, res, copyVaultSession(session));
|
|
3457
4193
|
await store.deleteSessionsByLogicalSessionId({ logicalSessionId: authority.logicalSessionId });
|
|
3458
|
-
|
|
3459
|
-
clearSessionCookie(res, options);
|
|
4194
|
+
try {
|
|
4195
|
+
if (usesCookieTransport(options)) clearSessionCookie(res, options);
|
|
4196
|
+
if (session === void 0 || !redirect && navigation === void 0) return {};
|
|
4197
|
+
if (navigation !== void 0 && !isString(session.idToken)) return {};
|
|
4198
|
+
try {
|
|
4199
|
+
const metadata = await resolveProviderMetadata(config, options);
|
|
4200
|
+
if (!metadata.endSessionEndpoint) return {};
|
|
4201
|
+
const endpoint = navigation === void 0 ? metadata.endSessionEndpoint : validateUpstreamLogoutUrl(metadata.endSessionEndpoint);
|
|
4202
|
+
const builtUrl = buildLogoutUrl(endpoint, session.idToken, options.postLogoutRedirectUri);
|
|
4203
|
+
if (navigation === void 0) return { upstreamLogoutUrl: builtUrl };
|
|
4204
|
+
const upstreamLogoutUrl = validateUpstreamLogoutUrl(builtUrl);
|
|
4205
|
+
const navigationUrl = await persistLogoutNavigation(navigation, store, upstreamLogoutUrl, getNow(options));
|
|
4206
|
+
return { navigationUrl };
|
|
4207
|
+
} catch (error) {
|
|
4208
|
+
await notifyLogoutError(options, req, res, error, session);
|
|
4209
|
+
return {};
|
|
4210
|
+
}
|
|
4211
|
+
} finally {
|
|
4212
|
+
if (session !== void 0)
|
|
4213
|
+
await callPostCommitHook("logout", options, options.hooks?.onLogout, req, res, copyVaultSession(session));
|
|
3460
4214
|
}
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
4215
|
+
};
|
|
4216
|
+
var createLogoutHandler = (options, config, trustedOrigins, proofVerifier, navigation) => createAsyncHandler("logout", options, async (req, res, _next, diagnostic) => {
|
|
4217
|
+
if (navigation === void 0) assertTrustedOrigin(req, options, trustedOrigins, "logout");
|
|
4218
|
+
else {
|
|
4219
|
+
assertLogoutNavigationRequest(req, options, trustedOrigins);
|
|
4220
|
+
const body = req.body;
|
|
4221
|
+
if (typeof body !== "object" || body === null || Array.isArray(body) || Object.keys(body).some((key) => key !== "sessionId") || Object.hasOwn(body, "sessionId") && typeof body.sessionId !== "string")
|
|
4222
|
+
throw new OidcVaultHttpError(
|
|
4223
|
+
400,
|
|
4224
|
+
"OIDC_VAULT_INVALID_REQUEST_BODY",
|
|
4225
|
+
"Logout navigation request body is invalid."
|
|
4226
|
+
);
|
|
4227
|
+
}
|
|
4228
|
+
const redirect = navigation === void 0 && getBody(req).redirect === true;
|
|
4229
|
+
const result = await performLogout(options, config, proofVerifier, req, res, diagnostic, redirect, navigation);
|
|
4230
|
+
applyCredentialResponseCachePolicy(res);
|
|
4231
|
+
if (navigation !== void 0) {
|
|
4232
|
+
res.status(200).json({
|
|
4233
|
+
loggedOut: true,
|
|
4234
|
+
navigationUrl: result.navigationUrl ?? navigation.navigationUrl
|
|
4235
|
+
});
|
|
3464
4236
|
return;
|
|
3465
4237
|
}
|
|
3466
|
-
if (
|
|
3467
|
-
|
|
3468
|
-
applyCredentialResponseCachePolicy(res);
|
|
3469
|
-
res.status(200).json({ loggedOut: true });
|
|
4238
|
+
if (result.upstreamLogoutUrl) {
|
|
4239
|
+
res.redirect(302, result.upstreamLogoutUrl);
|
|
3470
4240
|
return;
|
|
3471
4241
|
}
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
4242
|
+
res.status(200).json({ loggedOut: true });
|
|
4243
|
+
});
|
|
4244
|
+
var assertLogoutNavigationRequest = (req, options, trustedOrigins) => {
|
|
4245
|
+
captureTrustedOriginGuard(req, options, trustedOrigins, "logout", true)();
|
|
4246
|
+
if (!req.is("application/json"))
|
|
4247
|
+
throw new OidcVaultHttpError(
|
|
4248
|
+
415,
|
|
4249
|
+
"OIDC_VAULT_UNSUPPORTED_REQUEST_BODY_TYPE",
|
|
4250
|
+
"Logout navigation requires a JSON request body."
|
|
4251
|
+
);
|
|
4252
|
+
};
|
|
4253
|
+
var createLogoutNavigationGetHandler = (options) => async (req, res) => {
|
|
4254
|
+
const policy = options.logoutNavigation;
|
|
4255
|
+
let location = policy.fallbackRedirectUri;
|
|
4256
|
+
const ticketHash = navigationTicketHash(req.originalUrl);
|
|
4257
|
+
if (ticketHash !== void 0) {
|
|
3477
4258
|
try {
|
|
3478
|
-
await options.
|
|
3479
|
-
|
|
3480
|
-
|
|
4259
|
+
const record = await options.storeProvider.consumeLogoutNavigationTicket({
|
|
4260
|
+
namespace: policy.namespace,
|
|
4261
|
+
ticketHash
|
|
3481
4262
|
});
|
|
3482
|
-
|
|
4263
|
+
if (record !== null)
|
|
4264
|
+
location = consumedLogoutDestination(record, policy.namespace, ticketHash, getNow(options));
|
|
4265
|
+
} catch (error) {
|
|
4266
|
+
await notifyLogoutError(options, req, res, error);
|
|
3483
4267
|
}
|
|
3484
4268
|
}
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
res.redirect(302, upstreamLogoutUrl);
|
|
3489
|
-
return;
|
|
3490
|
-
}
|
|
3491
|
-
res.status(200).json({
|
|
3492
|
-
loggedOut: true
|
|
3493
|
-
});
|
|
3494
|
-
});
|
|
4269
|
+
res.status(302).setHeader("Location", location);
|
|
4270
|
+
res.end();
|
|
4271
|
+
};
|
|
3495
4272
|
var createBackchannelLogoutHandler = (options, config) => createAsyncHandler("backchannel-logout", options, async (req, res) => {
|
|
3496
4273
|
const metadata = await resolveProviderMetadata(config, options);
|
|
3497
4274
|
const logoutToken = getLogoutTokenFromRequest(req);
|
|
@@ -3553,6 +4330,10 @@ function registerRoutes(router, options, config, trustedOrigins, backendOrigin,
|
|
|
3553
4330
|
router.post(OIDC_VAULT_ROUTE_PATHS.exchange, createExchangeHandler(options, config, trustedOrigins, proofVerifier));
|
|
3554
4331
|
router.post(OIDC_VAULT_ROUTE_PATHS.refresh, createRefreshHandler(options, config, trustedOrigins, proofVerifier));
|
|
3555
4332
|
router.post(OIDC_VAULT_ROUTE_PATHS.logout, createLogoutHandler(options, config, trustedOrigins, proofVerifier));
|
|
4333
|
+
if (options.logoutNavigation !== void 0) {
|
|
4334
|
+
const path = OIDC_VAULT_ROUTE_PATHS["logout-navigation"];
|
|
4335
|
+
router.post(path, createLogoutHandler(options, config, trustedOrigins, proofVerifier, options.logoutNavigation));
|
|
4336
|
+
}
|
|
3556
4337
|
router.post(OIDC_VAULT_ROUTE_PATHS["backchannel-logout"], createBackchannelLogoutHandler(options, config));
|
|
3557
4338
|
}
|
|
3558
4339
|
function createOidcVaultMiddleware(options) {
|
|
@@ -3565,6 +4346,26 @@ function createOidcVaultMiddleware(options) {
|
|
|
3565
4346
|
applyCredentialResponseCachePolicy(res);
|
|
3566
4347
|
next();
|
|
3567
4348
|
});
|
|
4349
|
+
if (resolvedOptions.logoutNavigation !== void 0) {
|
|
4350
|
+
const path = OIDC_VAULT_ROUTE_PATHS["logout-navigation"];
|
|
4351
|
+
baseRouter.all(path, (_req, res, next) => {
|
|
4352
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
4353
|
+
next();
|
|
4354
|
+
});
|
|
4355
|
+
baseRouter.head(path, (_req, res) => {
|
|
4356
|
+
res.setHeader("Allow", "GET, POST");
|
|
4357
|
+
res.status(405).end();
|
|
4358
|
+
});
|
|
4359
|
+
baseRouter.get(path, createLogoutNavigationGetHandler(resolvedOptions));
|
|
4360
|
+
baseRouter.post(
|
|
4361
|
+
path,
|
|
4362
|
+
createAsyncHandler("logout", resolvedOptions, async (req, _res, next) => {
|
|
4363
|
+
assertLogoutNavigationRequest(req, resolvedOptions, trustedOrigins);
|
|
4364
|
+
next();
|
|
4365
|
+
})
|
|
4366
|
+
);
|
|
4367
|
+
baseRouter.post(path, import_express.default.json({ limit: requestBodyLimit, strict: false }));
|
|
4368
|
+
}
|
|
3568
4369
|
if (resolvedOptions.deviceBinding !== void 0 || resolvedOptions.fingerprintRecognition !== void 0) {
|
|
3569
4370
|
baseRouter.post(
|
|
3570
4371
|
OIDC_VAULT_ROUTE_PATHS.login,
|
|
@@ -3596,9 +4397,14 @@ function createOidcVaultMiddleware(options) {
|
|
|
3596
4397
|
DEFAULT_OIDC_VAULT_REQUEST_BODY_LIMIT,
|
|
3597
4398
|
OIDC_VAULT_ROUTE_PATHS,
|
|
3598
4399
|
OIDC_VAULT_URL_ENCODED_PARAMETER_LIMIT,
|
|
4400
|
+
OidcVaultAuthDeniedError,
|
|
3599
4401
|
OidcVaultDpopReplayCapacityError,
|
|
4402
|
+
OidcVaultInvalidAccessTokenError,
|
|
4403
|
+
OidcVaultLogoutNavigationCapacityError,
|
|
3600
4404
|
OidcVaultStoreConflictError,
|
|
3601
4405
|
createOidcVaultAccessTokenMiddleware,
|
|
4406
|
+
createOidcVaultAuthenticator,
|
|
4407
|
+
createOidcVaultJwtAccessTokenIssuer,
|
|
3602
4408
|
createOidcVaultJwtAccessTokenValidator,
|
|
3603
4409
|
createOidcVaultMiddleware,
|
|
3604
4410
|
normalizeOidcVaultBasePath,
|