@blamejs/blamejs-shop 0.4.85 → 0.4.86
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/CHANGELOG.md +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/order.js +4 -2
- package/lib/security-middleware.js +1 -0
- package/lib/vendor/MANIFEST.json +71 -39
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/api-snapshot.json +11 -3
- package/lib/vendor/blamejs/lib/atomic-file.js +34 -0
- package/lib/vendor/blamejs/lib/auth/fido-mds3.js +10 -0
- package/lib/vendor/blamejs/lib/auth/password.js +1 -0
- package/lib/vendor/blamejs/lib/auth/saml.js +11 -3
- package/lib/vendor/blamejs/lib/daemon.js +4 -1
- package/lib/vendor/blamejs/lib/external-db.js +131 -0
- package/lib/vendor/blamejs/lib/graphql-federation.js +25 -15
- package/lib/vendor/blamejs/lib/log-stream-cloudwatch.js +1 -0
- package/lib/vendor/blamejs/lib/log-stream-local.js +14 -1
- package/lib/vendor/blamejs/lib/log-stream-otlp.js +1 -0
- package/lib/vendor/blamejs/lib/log-stream-webhook.js +1 -0
- package/lib/vendor/blamejs/lib/mail-auth.js +69 -14
- package/lib/vendor/blamejs/lib/mail-bimi.js +6 -0
- package/lib/vendor/blamejs/lib/mail-crypto-smime.js +10 -0
- package/lib/vendor/blamejs/lib/mail-dkim.js +68 -15
- package/lib/vendor/blamejs/lib/mail.js +39 -0
- package/lib/vendor/blamejs/lib/middleware/api-encrypt.js +6 -2
- package/lib/vendor/blamejs/lib/network-dns-resolver.js +61 -11
- package/lib/vendor/blamejs/lib/network-dns.js +47 -2
- package/lib/vendor/blamejs/lib/network-nts.js +16 -0
- package/lib/vendor/blamejs/lib/network-proxy.js +55 -2
- package/lib/vendor/blamejs/lib/object-store/azure-blob-bucket-ops.js +1 -0
- package/lib/vendor/blamejs/lib/object-store/gcs-bucket-ops.js +1 -0
- package/lib/vendor/blamejs/lib/object-store/http-request.js +4 -0
- package/lib/vendor/blamejs/lib/outbox.js +29 -0
- package/lib/vendor/blamejs/lib/queue-sqs.js +1 -0
- package/lib/vendor/blamejs/lib/request-helpers.js +25 -6
- package/lib/vendor/blamejs/lib/session-device-binding.js +46 -24
- package/lib/vendor/blamejs/lib/session.js +85 -28
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.15.16.json +94 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/api-encrypt.test.js +51 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/atomic-file-open-append-nofollow.test.js +87 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +48 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/external-db-non-atomic-backend.test.js +200 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/fido-mds3-cert-bad-validity.test.js +159 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/graphql-federation.test.js +49 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-auth-dmarc-policy-failclosed.test.js +139 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-bimi-cert-validity-unparseable.test.js +137 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-crypto-smime-bad-validity.test.js +134 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-dkim-numericdate-failclosed.test.js +155 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-proxy-framing-bounds.test.js +263 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-lookup-timeout-default.test.js +116 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-dns-resolver-timeout.test.js +126 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/network-nts-handshake-byte-cap.test.js +127 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/saml-subjectconfirmation-notbefore.test.js +238 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-destroy-all-store-backed.test.js +128 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding-ipv6-canonical-and-no-store.test.js +202 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/session-device-binding.test.js +18 -9
- package/lib/vendor/blamejs/test/layer-0-primitives/webhook-verify-nonce-atomic.test.js +169 -0
- package/lib/webhooks.js +38 -9
- package/package.json +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.* NTS-KE handshake byte-cap.
|
|
4
|
+
*
|
|
5
|
+
* The NTS-KE (RFC 8915) key-establishment handshake reader accumulates
|
|
6
|
+
* received bytes until a REC_END record terminates the exchange. A
|
|
7
|
+
* wall-clock timer bounds the handshake by time, but NOT by memory: a
|
|
8
|
+
* server that streams non-END records fast enough OOMs the process
|
|
9
|
+
* before the timer fires. The reader must cap the accumulated buffer at
|
|
10
|
+
* a sane ceiling (64 KiB) and fail closed with an nts/* typed error.
|
|
11
|
+
*
|
|
12
|
+
* This drives the real consumer path — network-nts.performKeHandshake
|
|
13
|
+
* against a live loopback TLS server negotiating ALPN "ntske/1" that
|
|
14
|
+
* streams > 64 KiB of non-END records.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
var helpers = require("../helpers");
|
|
18
|
+
var check = helpers.check;
|
|
19
|
+
var nts = require("../../lib/network-nts");
|
|
20
|
+
var nodeTls = require("node:tls");
|
|
21
|
+
|
|
22
|
+
var REC_NEW_COOKIE = 5;
|
|
23
|
+
|
|
24
|
+
// Encode an NTS-KE record: u16 type (top bit = critical) || u16 length
|
|
25
|
+
// || body. We emit non-critical NEW_COOKIE records that never include a
|
|
26
|
+
// REC_END (type 0), so a conformant reader keeps accumulating.
|
|
27
|
+
function _encodeRecord(type, body) {
|
|
28
|
+
var hdr = Buffer.alloc(4);
|
|
29
|
+
hdr.writeUInt16BE(type & 0x7fff, 0);
|
|
30
|
+
hdr.writeUInt16BE(body.length, 2);
|
|
31
|
+
return Buffer.concat([hdr, body]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function testHandshakeByteCapFailsClosed() {
|
|
35
|
+
return new Promise(function (resolve, reject) {
|
|
36
|
+
(async function () {
|
|
37
|
+
var server = null;
|
|
38
|
+
try {
|
|
39
|
+
var ca = await helpers.b.mtlsEngine.generateCa({ name: "nts-test-ca" });
|
|
40
|
+
var leaf = await helpers.b.mtlsEngine.signClientCert({
|
|
41
|
+
cn: "localhost",
|
|
42
|
+
caCertPem: ca.caCertPem,
|
|
43
|
+
caKeyPem: ca.caKeyPem,
|
|
44
|
+
usage: "server",
|
|
45
|
+
sans: ["DNS:localhost", "IP:127.0.0.1"],
|
|
46
|
+
validityDays: 1,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// One ~1 KiB non-END record; streamed enough times to exceed the
|
|
50
|
+
// 64 KiB ceiling without ever sending REC_END.
|
|
51
|
+
var oneRecord = _encodeRecord(REC_NEW_COOKIE, Buffer.alloc(1020, 0x41));
|
|
52
|
+
var TOTAL_BYTES = 128 * 1024; // 2x the 64 KiB cap
|
|
53
|
+
|
|
54
|
+
server = nodeTls.createServer({
|
|
55
|
+
key: leaf.key,
|
|
56
|
+
cert: leaf.cert,
|
|
57
|
+
minVersion: "TLSv1.3",
|
|
58
|
+
maxVersion: "TLSv1.3",
|
|
59
|
+
ALPNProtocols: ["ntske/1"],
|
|
60
|
+
}, function (sock) {
|
|
61
|
+
sock.on("error", function () { /* client tears down mid-stream */ });
|
|
62
|
+
var sent = 0;
|
|
63
|
+
function pump() {
|
|
64
|
+
while (sent < TOTAL_BYTES) {
|
|
65
|
+
sent += oneRecord.length;
|
|
66
|
+
if (!sock.write(oneRecord)) {
|
|
67
|
+
sock.once("drain", pump);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
pump();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
server.listen(0, "127.0.0.1", function () {
|
|
76
|
+
var port = server.address().port;
|
|
77
|
+
var startedAt = Date.now();
|
|
78
|
+
nts.performKeHandshake({
|
|
79
|
+
host: "127.0.0.1",
|
|
80
|
+
port: port,
|
|
81
|
+
servername: "localhost",
|
|
82
|
+
ca: ca.caCertPem,
|
|
83
|
+
// Generous timeout: a passing run must reject on the byte cap
|
|
84
|
+
// FAST (well under this), proving the cap — not the timer —
|
|
85
|
+
// bounded the read.
|
|
86
|
+
timeoutMs: 30000,
|
|
87
|
+
}).then(function () {
|
|
88
|
+
server.close();
|
|
89
|
+
reject(new Error("handshake resolved despite > 64 KiB of non-END records"));
|
|
90
|
+
}).catch(function (e) {
|
|
91
|
+
var elapsed = Date.now() - startedAt;
|
|
92
|
+
try { server.close(); } catch (_e) { /* best-effort */ }
|
|
93
|
+
try {
|
|
94
|
+
check("handshake rejects on oversized stream",
|
|
95
|
+
e && e.code === "nts/ke-too-large");
|
|
96
|
+
check("rejection is NtsError typed",
|
|
97
|
+
e instanceof nts.NtsError);
|
|
98
|
+
check("rejection is bounded, not the wall-clock timeout",
|
|
99
|
+
e.code !== "nts/ke-timeout");
|
|
100
|
+
check("bounded fast (well under the 30s timer)",
|
|
101
|
+
elapsed < 15000);
|
|
102
|
+
resolve();
|
|
103
|
+
} catch (assertErr) {
|
|
104
|
+
reject(assertErr);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
server.on("error", function (e) { reject(e); });
|
|
110
|
+
} catch (e) {
|
|
111
|
+
if (server) { try { server.close(); } catch (_e) { /* best-effort */ } }
|
|
112
|
+
reject(e);
|
|
113
|
+
}
|
|
114
|
+
})();
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function run() {
|
|
119
|
+
await testHandshakeByteCapFailsClosed();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { run: run };
|
|
123
|
+
|
|
124
|
+
if (require.main === module) {
|
|
125
|
+
run().then(function () { console.log("OK"); })
|
|
126
|
+
.catch(function (e) { console.error(e.stack || e); process.exit(1); });
|
|
127
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.auth.saml.sp.verifyResponse — Bearer SubjectConfirmationData
|
|
4
|
+
* NotBefore must fail closed when present-but-unparseable.
|
|
5
|
+
*
|
|
6
|
+
* SAML 2.0 Web Browser SSO Profile §4.1.4.2: a Bearer
|
|
7
|
+
* SubjectConfirmationData MAY carry a `NotBefore` bounding the start of
|
|
8
|
+
* its validity window. When NotBefore is PRESENT, the verifier must
|
|
9
|
+
* honor it. A NotBefore that cannot be parsed (Date.parse → NaN) is an
|
|
10
|
+
* IdP error or a tampered attribute — it cannot establish that the
|
|
11
|
+
* confirmation has begun, so it must NOT be silently dropped, leaving
|
|
12
|
+
* the SCD accepted. It must fail closed (the confirmation is skipped,
|
|
13
|
+
* the verifier tries the next one, and an assertion with no other valid
|
|
14
|
+
* confirmation is refused) — mirroring the NotOnOrAfter line just above
|
|
15
|
+
* and the already-hardened Conditions block.
|
|
16
|
+
*
|
|
17
|
+
* These tests drive the shipped consumer path:
|
|
18
|
+
* sp = b.auth.saml.sp.create({ ... }); sp.verifyResponse(b64, vopts).
|
|
19
|
+
* They mint a self-signed RSA IdP cert, build a genuinely XMLDSig-
|
|
20
|
+
* signed SAML Response (digest + SignatureValue computed through the
|
|
21
|
+
* framework's own b.xmlC14n so the verifier's recomputation matches),
|
|
22
|
+
* and vary ONLY the SubjectConfirmationData's NotBefore between the
|
|
23
|
+
* accepted and refused cases.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
var helpers = require("../helpers");
|
|
27
|
+
var check = helpers.check;
|
|
28
|
+
var b = helpers.b;
|
|
29
|
+
var nodeCrypto = require("node:crypto");
|
|
30
|
+
var c14n = require("../../lib/xml-c14n");
|
|
31
|
+
|
|
32
|
+
var DS = "http://www.w3.org/2000/09/xmldsig#";
|
|
33
|
+
var EXC = "http://www.w3.org/2001/10/xml-exc-c14n#";
|
|
34
|
+
|
|
35
|
+
var IDP_ENTITY_ID = "https://idp.example";
|
|
36
|
+
var SP_ENTITY_ID = "https://sp.example";
|
|
37
|
+
var ACS_URL = "https://sp.example/saml/acs";
|
|
38
|
+
|
|
39
|
+
// Mint a self-signed RSA cert via the vendored @peculiar/x509 bundle.
|
|
40
|
+
// verifyResponse parses idpCertPem with nodeCrypto.createPublicKey and
|
|
41
|
+
// verifies an rsa-sha256 PKCS1 signature, so a real RSA cert + matching
|
|
42
|
+
// private key is required — there is no test bypass of the signature
|
|
43
|
+
// check.
|
|
44
|
+
async function _mintRsaCert(cn) {
|
|
45
|
+
var pki = require("../../lib/vendor/pki.cjs");
|
|
46
|
+
var x509 = pki.x509;
|
|
47
|
+
var keys = await nodeCrypto.webcrypto.subtle.generateKey(
|
|
48
|
+
{ name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, // allow:raw-byte-literal — RFC 8301 §3.1 RSA bit floor
|
|
49
|
+
publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" },
|
|
50
|
+
true, ["sign", "verify"]);
|
|
51
|
+
var now = new Date();
|
|
52
|
+
var cert = await x509.X509CertificateGenerator.createSelfSigned({
|
|
53
|
+
serialNumber: "01",
|
|
54
|
+
name: "CN=" + cn,
|
|
55
|
+
notBefore: now,
|
|
56
|
+
notAfter: new Date(now.getTime() + 365 * 24 * 3600 * 1000), // allow:raw-time-literal — 1y fixture validity
|
|
57
|
+
signingAlgorithm: { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
58
|
+
keys: keys,
|
|
59
|
+
});
|
|
60
|
+
var pkcs8 = await nodeCrypto.webcrypto.subtle.exportKey("pkcs8", keys.privateKey);
|
|
61
|
+
var keyPem = "-----BEGIN PRIVATE KEY-----\n" +
|
|
62
|
+
Buffer.from(pkcs8).toString("base64").match(/.{1,64}/g).join("\n") +
|
|
63
|
+
"\n-----END PRIVATE KEY-----\n";
|
|
64
|
+
return { certPem: cert.toString("pem"), keyPem: keyPem };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function _isoFromNow(ms) { return new Date(Date.now() + ms).toISOString(); }
|
|
68
|
+
|
|
69
|
+
// Build a SAML Response with an Assertion-level enveloped XMLDSig
|
|
70
|
+
// signature. The Assertion's Signature child is stripped before the
|
|
71
|
+
// digest (the enveloped-signature transform), and both the digest and
|
|
72
|
+
// the SignedInfo are canonicalized through b.xmlC14n so the values
|
|
73
|
+
// match exactly what verifyResponse recomputes.
|
|
74
|
+
function _buildSignedResponse(idp, parts) {
|
|
75
|
+
var assertionId = "_assertion-" + parts.tag;
|
|
76
|
+
var responseId = "_response-" + parts.tag;
|
|
77
|
+
var issueInstant = _isoFromNow(0);
|
|
78
|
+
var subjectConfirmation =
|
|
79
|
+
"<saml:SubjectConfirmation Method=\"" + parts.method + "\">" +
|
|
80
|
+
parts.scd +
|
|
81
|
+
"</saml:SubjectConfirmation>";
|
|
82
|
+
|
|
83
|
+
var assertionInner =
|
|
84
|
+
"<saml:Issuer>" + IDP_ENTITY_ID + "</saml:Issuer>" +
|
|
85
|
+
"SIGNATURE_PLACEHOLDER" +
|
|
86
|
+
"<saml:Subject>" +
|
|
87
|
+
"<saml:NameID Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\">" +
|
|
88
|
+
parts.nameId + "</saml:NameID>" +
|
|
89
|
+
subjectConfirmation +
|
|
90
|
+
"</saml:Subject>" +
|
|
91
|
+
(parts.conditions !== undefined ? parts.conditions :
|
|
92
|
+
"<saml:Conditions NotBefore=\"" + _isoFromNow(-5 * 60 * 1000) + // allow:raw-time-literal — 5m skew window
|
|
93
|
+
"\" NotOnOrAfter=\"" + _isoFromNow(5 * 60 * 1000) + "\">" + // allow:raw-time-literal — 5m skew window
|
|
94
|
+
"<saml:AudienceRestriction><saml:Audience>" + SP_ENTITY_ID +
|
|
95
|
+
"</saml:Audience></saml:AudienceRestriction>" +
|
|
96
|
+
"</saml:Conditions>") +
|
|
97
|
+
"<saml:AuthnStatement SessionIndex=\"_sess-1\" AuthnInstant=\"" + issueInstant + "\">" +
|
|
98
|
+
"<saml:AuthnContext><saml:AuthnContextClassRef>" +
|
|
99
|
+
"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" +
|
|
100
|
+
"</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement>";
|
|
101
|
+
|
|
102
|
+
var assertionOpen =
|
|
103
|
+
"<saml:Assertion xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" " +
|
|
104
|
+
"ID=\"" + assertionId + "\" Version=\"2.0\" IssueInstant=\"" + issueInstant + "\">";
|
|
105
|
+
var assertionClose = "</saml:Assertion>";
|
|
106
|
+
|
|
107
|
+
// Digest the assertion with no Signature child (enveloped-signature
|
|
108
|
+
// transform output), canonicalized through b.xmlC14n.
|
|
109
|
+
var assertionNoSig = assertionOpen +
|
|
110
|
+
assertionInner.replace("SIGNATURE_PLACEHOLDER", "") + assertionClose;
|
|
111
|
+
var digest = nodeCrypto.createHash("sha256")
|
|
112
|
+
.update(c14n.canonicalize(assertionNoSig)).digest("base64");
|
|
113
|
+
|
|
114
|
+
var signedInfo =
|
|
115
|
+
"<ds:SignedInfo xmlns:ds=\"" + DS + "\">" +
|
|
116
|
+
"<ds:CanonicalizationMethod Algorithm=\"" + EXC + "\"></ds:CanonicalizationMethod>" +
|
|
117
|
+
"<ds:SignatureMethod Algorithm=\"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256\"></ds:SignatureMethod>" +
|
|
118
|
+
"<ds:Reference URI=\"#" + assertionId + "\">" +
|
|
119
|
+
"<ds:Transforms>" +
|
|
120
|
+
"<ds:Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature\"></ds:Transform>" +
|
|
121
|
+
"<ds:Transform Algorithm=\"" + EXC + "\"></ds:Transform>" +
|
|
122
|
+
"</ds:Transforms>" +
|
|
123
|
+
"<ds:DigestMethod Algorithm=\"http://www.w3.org/2001/04/xmlenc#sha256\"></ds:DigestMethod>" +
|
|
124
|
+
"<ds:DigestValue>" + digest + "</ds:DigestValue>" +
|
|
125
|
+
"</ds:Reference>" +
|
|
126
|
+
"</ds:SignedInfo>";
|
|
127
|
+
var priv = nodeCrypto.createPrivateKey({ key: idp.keyPem, format: "pem" });
|
|
128
|
+
var sigValue = nodeCrypto.sign("sha256", c14n.canonicalize(signedInfo),
|
|
129
|
+
{ key: priv, padding: nodeCrypto.constants.RSA_PKCS1_PADDING }).toString("base64");
|
|
130
|
+
|
|
131
|
+
var signatureXml =
|
|
132
|
+
"<ds:Signature xmlns:ds=\"" + DS + "\">" + signedInfo +
|
|
133
|
+
"<ds:SignatureValue>" + sigValue + "</ds:SignatureValue></ds:Signature>";
|
|
134
|
+
var assertionFull = assertionOpen +
|
|
135
|
+
assertionInner.replace("SIGNATURE_PLACEHOLDER", signatureXml) + assertionClose;
|
|
136
|
+
|
|
137
|
+
var response =
|
|
138
|
+
"<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" " +
|
|
139
|
+
"xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" " +
|
|
140
|
+
"ID=\"" + responseId + "\" Version=\"2.0\" IssueInstant=\"" + issueInstant + "\" " +
|
|
141
|
+
"Destination=\"" + ACS_URL + "\">" +
|
|
142
|
+
"<saml:Issuer>" + IDP_ENTITY_ID + "</saml:Issuer>" +
|
|
143
|
+
"<samlp:Status><samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\"/></samlp:Status>" +
|
|
144
|
+
assertionFull + "</samlp:Response>";
|
|
145
|
+
|
|
146
|
+
return Buffer.from(response, "utf8").toString("base64");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function _bearerScd(notBeforeAttr, inResponseTo) {
|
|
150
|
+
return "<saml:SubjectConfirmationData" +
|
|
151
|
+
" NotOnOrAfter=\"" + _isoFromNow(5 * 60 * 1000) + "\"" + // allow:raw-time-literal — 5m future, otherwise-valid window
|
|
152
|
+
notBeforeAttr +
|
|
153
|
+
" Recipient=\"" + ACS_URL + "\"" +
|
|
154
|
+
" InResponseTo=\"" + inResponseTo + "\"/>";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function _newSp(idp) {
|
|
158
|
+
return b.auth.saml.sp.create({
|
|
159
|
+
entityId: SP_ENTITY_ID,
|
|
160
|
+
assertionConsumerServiceUrl: ACS_URL,
|
|
161
|
+
idpEntityId: IDP_ENTITY_ID,
|
|
162
|
+
idpSsoUrl: "https://idp.example/sso",
|
|
163
|
+
idpCertPem: idp.certPem,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function _verifyThrows(sp, b64, vopts) {
|
|
168
|
+
try { sp.verifyResponse(b64, vopts); return null; }
|
|
169
|
+
catch (e) { return e.code || e.message; }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Case 1 — a past (already-valid) NotBefore succeeds. Proves the signer
|
|
173
|
+
// + harness are correct and the fail-closed fix does not break the happy
|
|
174
|
+
// path (an otherwise-acceptable confirmation that has begun).
|
|
175
|
+
async function testBearerPastNotBeforeAccepted() {
|
|
176
|
+
var idp = await _mintRsaCert("idp.example");
|
|
177
|
+
var sp = _newSp(idp);
|
|
178
|
+
var inResponseTo = "_req-past-nb";
|
|
179
|
+
var b64 = _buildSignedResponse(idp, {
|
|
180
|
+
tag: "bearer-past-nb",
|
|
181
|
+
method: "urn:oasis:names:tc:SAML:2.0:cm:bearer",
|
|
182
|
+
nameId: "alice@example.com",
|
|
183
|
+
scd: _bearerScd(" NotBefore=\"" + _isoFromNow(-5 * 60 * 1000) + "\"", inResponseTo), // allow:raw-time-literal — 5m past, already valid
|
|
184
|
+
});
|
|
185
|
+
var info = sp.verifyResponse(b64, { expectedInResponseTo: inResponseTo });
|
|
186
|
+
check("Bearer with past NotBefore verifies", info && typeof info === "object");
|
|
187
|
+
check("Bearer past NotBefore: nameId returned", info.nameId === "alice@example.com");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Case 2 — a NotBefore in the FUTURE (not yet valid) must be refused.
|
|
191
|
+
// This path already fails closed on the unfixed tree (isFinite(nb) is
|
|
192
|
+
// true and nb > now), so it is the harness-correctness control proving
|
|
193
|
+
// the not-yet-valid axis is enforced at all.
|
|
194
|
+
async function testBearerFutureNotBeforeRefused() {
|
|
195
|
+
var idp = await _mintRsaCert("idp.example");
|
|
196
|
+
var sp = _newSp(idp);
|
|
197
|
+
var inResponseTo = "_req-future-nb";
|
|
198
|
+
var b64 = _buildSignedResponse(idp, {
|
|
199
|
+
tag: "bearer-future-nb",
|
|
200
|
+
method: "urn:oasis:names:tc:SAML:2.0:cm:bearer",
|
|
201
|
+
nameId: "alice@example.com",
|
|
202
|
+
scd: _bearerScd(" NotBefore=\"" + _isoFromNow(60 * 60 * 1000) + "\"", inResponseTo), // allow:raw-time-literal — 1h future, not yet valid
|
|
203
|
+
});
|
|
204
|
+
check("Bearer with future NotBefore is refused (not yet valid)",
|
|
205
|
+
_verifyThrows(sp, b64, { expectedInResponseTo: inResponseTo }) === "auth-saml/no-valid-confirmation");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Case 3 — the RED case: a Bearer SubjectConfirmationData with a
|
|
209
|
+
// present-but-UNPARSEABLE NotBefore. On the unfixed tree `isFinite(nb)`
|
|
210
|
+
// is false, so the `&&`-guarded not-yet-valid check is skipped and the
|
|
211
|
+
// SCD is wrongly ACCEPTED. It must fail closed: an unparseable NotBefore
|
|
212
|
+
// cannot establish that the confirmation has begun, so the SCD is
|
|
213
|
+
// skipped and the assertion (with no other valid confirmation) refused.
|
|
214
|
+
async function testBearerUnparseableNotBeforeRefused() {
|
|
215
|
+
var idp = await _mintRsaCert("idp.example");
|
|
216
|
+
var sp = _newSp(idp);
|
|
217
|
+
var inResponseTo = "_req-bad-nb";
|
|
218
|
+
var b64 = _buildSignedResponse(idp, {
|
|
219
|
+
tag: "bearer-bad-nb",
|
|
220
|
+
method: "urn:oasis:names:tc:SAML:2.0:cm:bearer",
|
|
221
|
+
nameId: "alice@example.com",
|
|
222
|
+
scd: _bearerScd(" NotBefore=\"not-a-date\"", inResponseTo),
|
|
223
|
+
});
|
|
224
|
+
check("Bearer unparseable NotBefore is refused (fail-closed)",
|
|
225
|
+
_verifyThrows(sp, b64, { expectedInResponseTo: inResponseTo }) === "auth-saml/no-valid-confirmation");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function run() {
|
|
229
|
+
await testBearerPastNotBeforeAccepted();
|
|
230
|
+
await testBearerFutureNotBeforeRefused();
|
|
231
|
+
await testBearerUnparseableNotBeforeRefused();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (require.main === module) {
|
|
235
|
+
run().then(function () { process.exit(0); })
|
|
236
|
+
.catch(function (e) { console.error(e); process.exit(1); });
|
|
237
|
+
}
|
|
238
|
+
module.exports = { run: run };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.session.destroyAllForUser must succeed for a store-backed-only consumer
|
|
4
|
+
* (b.session.useStore) that never called b.db.init() (#340).
|
|
5
|
+
*
|
|
6
|
+
* A store-backed-only deployment points session data at an isolated localDbThin
|
|
7
|
+
* file and registers the _blamejs_sessions sealed/derived-hash schema directly
|
|
8
|
+
* (so sealRow / lookupHash work) WITHOUT initializing the framework db. Every
|
|
9
|
+
* revoke-all path — logout-everywhere, suspend, delete, role change — calls
|
|
10
|
+
* destroyAllForUser, which deletes the store rows and then raises the stateless
|
|
11
|
+
* valid-from boundary via bump(). The boundary table lives in the framework db,
|
|
12
|
+
* so an uninitialized db threw db/not-initialized (a regression: worked in
|
|
13
|
+
* 0.15.11, broke when bump() was added) and 500'd the revoke even though the
|
|
14
|
+
* store rows were already deleted.
|
|
15
|
+
*
|
|
16
|
+
* RED on the pre-fix tree: destroyAllForUser throws (db/not-initialized rewrapped
|
|
17
|
+
* to MISCONFIGURED). GREEN: it resolves AND the stateless valid-from boundary is
|
|
18
|
+
* honored (routed through the configured store), so b.session.check revokes a
|
|
19
|
+
* token issued before the boundary.
|
|
20
|
+
*
|
|
21
|
+
* Drives the real consumer path through the public b.session surface — no db.init.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
var helpers = require("../helpers");
|
|
25
|
+
var b = helpers.b;
|
|
26
|
+
var check = helpers.check;
|
|
27
|
+
var setupVaultOnly = helpers.setupVaultOnly;
|
|
28
|
+
var teardownVaultOnly = helpers.teardownVaultOnly;
|
|
29
|
+
var fs = require("fs");
|
|
30
|
+
var os = require("os");
|
|
31
|
+
var path = require("path");
|
|
32
|
+
|
|
33
|
+
// Mirror the framework's _blamejs_sessions sealed/derived schema (db.js
|
|
34
|
+
// FRAMEWORK_SCHEMA) so sealRow / lookupHash work without b.db.init().
|
|
35
|
+
function _registerSessionSchema() {
|
|
36
|
+
b.cryptoField.registerTable("_blamejs_sessions", {
|
|
37
|
+
sealedFields: ["userId", "data"],
|
|
38
|
+
derivedHashes: { userIdHash: { from: "userId" } },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function run() {
|
|
43
|
+
var tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "blamejs-das-"));
|
|
44
|
+
var storeFile = path.join(tmpDir, "sessions.db");
|
|
45
|
+
var store = null;
|
|
46
|
+
try {
|
|
47
|
+
// Vault only — NO b.db.init(). This is the store-backed-only deployment.
|
|
48
|
+
await setupVaultOnly(tmpDir);
|
|
49
|
+
b.cluster._resetForTest();
|
|
50
|
+
b.session._resetForTest();
|
|
51
|
+
b.cryptoField.clearForTest();
|
|
52
|
+
_registerSessionSchema();
|
|
53
|
+
|
|
54
|
+
store = b.session.stores.localDbThin({ file: storeFile, audit: false });
|
|
55
|
+
b.session.useStore(store);
|
|
56
|
+
|
|
57
|
+
var s = b.session;
|
|
58
|
+
var uid = "user-340";
|
|
59
|
+
|
|
60
|
+
// Create a real store-backed session so destroyAllForUser has a row to
|
|
61
|
+
// delete (drives sealRow + the store INSERT — the shipped consumer path).
|
|
62
|
+
var created = await s.create({ userId: uid });
|
|
63
|
+
check("create returns a sealed cookie token", typeof created.token === "string" && created.token.length > 0);
|
|
64
|
+
|
|
65
|
+
// A token issued NOW (before any revoke) is valid against the boundary.
|
|
66
|
+
var iatBefore = Date.now();
|
|
67
|
+
check("a token issued before any revoke passes session.check",
|
|
68
|
+
(await s.check(uid, iatBefore)) === true);
|
|
69
|
+
check("a fresh subject's valid-from boundary is 0", (await s.validFrom(uid)) === 0);
|
|
70
|
+
|
|
71
|
+
// The bug: destroyAllForUser deletes the store rows, then bump() routes the
|
|
72
|
+
// valid-from upsert to the framework db (uninitialized) and throws. With the
|
|
73
|
+
// fix it falls back to the configured store and resolves.
|
|
74
|
+
var revoked = null;
|
|
75
|
+
var threw = null;
|
|
76
|
+
try {
|
|
77
|
+
revoked = await s.destroyAllForUser(uid);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
threw = e;
|
|
80
|
+
}
|
|
81
|
+
check("destroyAllForUser resolves for a store-backed-only consumer (no db.init)",
|
|
82
|
+
threw === null);
|
|
83
|
+
if (threw) {
|
|
84
|
+
check(" (pre-fix would throw db/not-initialized → MISCONFIGURED): " +
|
|
85
|
+
(threw.code || threw.message), false);
|
|
86
|
+
}
|
|
87
|
+
check("destroyAllForUser reports the deleted store-row count",
|
|
88
|
+
typeof revoked === "number" && revoked >= 1);
|
|
89
|
+
|
|
90
|
+
// The stateless valid-from boundary was actually raised (routed through the
|
|
91
|
+
// store), not silently dropped — a token issued before it is now revoked.
|
|
92
|
+
var boundary = await s.validFrom(uid);
|
|
93
|
+
check("the stateless valid-from boundary was raised (honored, not dropped)",
|
|
94
|
+
typeof boundary === "number" && boundary >= iatBefore);
|
|
95
|
+
check("a token issued BEFORE the revoke now fails session.check",
|
|
96
|
+
(await s.check(uid, iatBefore - 1000)) === false);
|
|
97
|
+
check("a token issued AFTER the boundary still passes session.check",
|
|
98
|
+
(await s.check(uid, boundary + 5000)) === true);
|
|
99
|
+
|
|
100
|
+
// Monotonicity still holds through the store backend.
|
|
101
|
+
var lower = await s.bump(uid, { epochMs: boundary - 5000 });
|
|
102
|
+
check("a lower-epoch bump is a monotonic no-op through the store",
|
|
103
|
+
lower === boundary);
|
|
104
|
+
|
|
105
|
+
// With NEITHER a framework db NOR a store, bump still fails closed (the
|
|
106
|
+
// boundary is never silently dropped — a real misconfiguration propagates).
|
|
107
|
+
b.session.useStore(null);
|
|
108
|
+
var threwNoStorage = null;
|
|
109
|
+
try {
|
|
110
|
+
await s.bump("user-no-storage");
|
|
111
|
+
} catch (e) { threwNoStorage = e; }
|
|
112
|
+
check("bump fails closed when neither a framework db nor a store exists",
|
|
113
|
+
threwNoStorage !== null);
|
|
114
|
+
} finally {
|
|
115
|
+
try { if (store && store.close) store.close(); } catch (_e) { /* best-effort */ }
|
|
116
|
+
b.session.useStore(null);
|
|
117
|
+
b.session._resetForTest();
|
|
118
|
+
b.cryptoField.clearForTest();
|
|
119
|
+
try { teardownVaultOnly(tmpDir); } catch (_e) { /* best-effort */ }
|
|
120
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_e) { /* best-effort */ }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { run: run };
|
|
125
|
+
if (require.main === module) {
|
|
126
|
+
run().then(function () { console.log("OK"); })
|
|
127
|
+
.catch(function (e) { console.error(e && e.stack ? e.stack : e); process.exit(1); });
|
|
128
|
+
}
|