@blamejs/core 0.7.107 → 0.8.4
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 +41 -1
- package/NOTICE +17 -1
- package/README.md +4 -3
- package/index.js +15 -0
- package/lib/asyncapi-bindings.js +160 -0
- package/lib/asyncapi-traits.js +143 -0
- package/lib/asyncapi.js +531 -0
- package/lib/audit-sign.js +1 -1
- package/lib/audit.js +68 -2
- package/lib/auth/acr-vocabulary.js +265 -0
- package/lib/auth/auth-time-tracker.js +111 -0
- package/lib/auth/elevation-grant.js +306 -0
- package/lib/auth/jwt.js +13 -0
- package/lib/auth/lockout.js +16 -3
- package/lib/auth/oauth.js +15 -1
- package/lib/auth/password.js +22 -2
- package/lib/auth/sd-jwt-vc-issuer.js +2 -2
- package/lib/auth/sd-jwt-vc.js +7 -2
- package/lib/auth/step-up-policy.js +335 -0
- package/lib/auth/step-up.js +445 -0
- package/lib/break-glass.js +53 -14
- package/lib/cache-redis.js +1 -1
- package/lib/cache.js +6 -1
- package/lib/cli.js +3 -3
- package/lib/cluster.js +24 -1
- package/lib/compliance-ai-act-logging.js +190 -0
- package/lib/compliance-ai-act-prohibited.js +205 -0
- package/lib/compliance-ai-act-risk.js +189 -0
- package/lib/compliance-ai-act-transparency.js +200 -0
- package/lib/compliance-ai-act.js +558 -0
- package/lib/compliance.js +12 -2
- package/lib/config-drift.js +2 -2
- package/lib/crypto-field.js +21 -1
- package/lib/crypto.js +114 -1
- package/lib/db.js +35 -4
- package/lib/dev.js +30 -3
- package/lib/dual-control.js +19 -1
- package/lib/external-db.js +10 -0
- package/lib/file-upload.js +30 -3
- package/lib/flag-cache.js +136 -0
- package/lib/flag-evaluation-context.js +135 -0
- package/lib/flag-providers.js +279 -0
- package/lib/flag-targeting.js +210 -0
- package/lib/flag.js +284 -0
- package/lib/guard-all.js +33 -16
- package/lib/guard-csv.js +16 -2
- package/lib/guard-html.js +35 -0
- package/lib/guard-svg.js +20 -0
- package/lib/http-client.js +57 -11
- package/lib/inbox.js +391 -0
- package/lib/log-stream-syslog.js +8 -0
- package/lib/log-stream.js +1 -1
- package/lib/mail-arc-sign.js +372 -0
- package/lib/mail-auth.js +2 -0
- package/lib/mail.js +40 -0
- package/lib/middleware/ai-act-disclosure.js +166 -0
- package/lib/middleware/asyncapi-serve.js +136 -0
- package/lib/middleware/attach-user.js +25 -2
- package/lib/middleware/bearer-auth.js +71 -6
- package/lib/middleware/body-parser.js +13 -0
- package/lib/middleware/cors.js +10 -0
- package/lib/middleware/csrf-protect.js +34 -3
- package/lib/middleware/dpop.js +3 -3
- package/lib/middleware/flag-context.js +76 -0
- package/lib/middleware/host-allowlist.js +1 -1
- package/lib/middleware/index.js +15 -0
- package/lib/middleware/openapi-serve.js +143 -0
- package/lib/middleware/require-aal.js +2 -2
- package/lib/middleware/require-step-up.js +186 -0
- package/lib/middleware/trace-propagate.js +1 -1
- package/lib/mtls-ca.js +23 -29
- package/lib/mtls-engine-default.js +21 -1
- package/lib/network-tls.js +21 -6
- package/lib/object-store/sigv4-bucket-ops.js +41 -0
- package/lib/observability-otlp-exporter.js +35 -2
- package/lib/openapi-paths-builder.js +248 -0
- package/lib/openapi-schema-walk.js +192 -0
- package/lib/openapi-security.js +169 -0
- package/lib/openapi-yaml.js +154 -0
- package/lib/openapi.js +443 -0
- package/lib/outbox.js +3 -3
- package/lib/permissions.js +10 -1
- package/lib/pqc-agent.js +22 -1
- package/lib/pqc-software.js +195 -0
- package/lib/pubsub.js +8 -4
- package/lib/redact.js +26 -1
- package/lib/retention.js +26 -0
- package/lib/router.js +1 -0
- package/lib/scheduler.js +57 -1
- package/lib/session.js +3 -3
- package/lib/ssrf-guard.js +19 -4
- package/lib/static.js +12 -0
- package/lib/totp.js +16 -0
- package/lib/vault/index.js +3 -0
- package/lib/vault-aad.js +259 -0
- package/lib/vendor/MANIFEST.json +29 -0
- package/lib/vendor/noble-post-quantum.cjs +18 -0
- package/lib/ws-client.js +978 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/lib/auth/jwt.js
CHANGED
|
@@ -147,6 +147,19 @@ async function sign(claims, opts) {
|
|
|
147
147
|
if (typeof opts.expiresInSec === "number" && payload.exp === undefined) {
|
|
148
148
|
payload.exp = nowSec + opts.expiresInSec;
|
|
149
149
|
}
|
|
150
|
+
// Auto-mint jti when the token has an expiry but no operator-set
|
|
151
|
+
// jti. The replay-defense path on verify() requires every replay-
|
|
152
|
+
// protected token to carry a jti; without auto-mint, an operator
|
|
153
|
+
// who configured replayStore on verify but forgot to set jti on
|
|
154
|
+
// sign produces tokens that never replay-protect — and the
|
|
155
|
+
// failure surfaces only at first replay attempt (via the
|
|
156
|
+
// verifier's "missing-jti" throw). Auto-mint closes the silent
|
|
157
|
+
// hole; operators who explicitly want a deterministic jti pass
|
|
158
|
+
// opts.jti themselves.
|
|
159
|
+
if (payload.exp !== undefined && payload.jti === undefined) {
|
|
160
|
+
var fwCryptoJti = require("../crypto"); // allow:inline-require — circular-load defense (crypto imports jwt? no — but use lazy form to keep parity)
|
|
161
|
+
payload.jti = fwCryptoJti.generateBytes(C.BYTES.bytes(16)).toString("base64url");
|
|
162
|
+
}
|
|
150
163
|
if (typeof opts.notBeforeSec === "number" && payload.nbf === undefined) {
|
|
151
164
|
payload.nbf = nowSec + opts.notBeforeSec;
|
|
152
165
|
}
|
package/lib/auth/lockout.js
CHANGED
|
@@ -227,12 +227,25 @@ function create(opts) {
|
|
|
227
227
|
} catch (_e) { /* audit best-effort */ }
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
+
// Cache failures fail-OPEN by design (per the framework's
|
|
231
|
+
// documented brute-force-lockout posture — rather than crash the
|
|
232
|
+
// request, allow the attempt). The signal MUST land somewhere
|
|
233
|
+
// visible regardless of operator wiring: observability picks it up
|
|
234
|
+
// when wired, and audit picks it up when wired. Without the audit
|
|
235
|
+
// path a deployment running with no observability + a degraded
|
|
236
|
+
// cache silently gets brute-force-protection-disabled.
|
|
237
|
+
function _signalCacheError(op) {
|
|
238
|
+
_emitObs("auth.lockout.cache_error", { namespace: namespace, op: op });
|
|
239
|
+
_emitAudit("auth.lockout.cache_error", "<system>", "failure",
|
|
240
|
+
{ namespace: namespace, op: op }, null);
|
|
241
|
+
}
|
|
242
|
+
|
|
230
243
|
async function _readState(key) {
|
|
231
244
|
try {
|
|
232
245
|
var raw = await cache.get(_scopedKey(key));
|
|
233
246
|
return raw || null;
|
|
234
247
|
} catch (_e) {
|
|
235
|
-
|
|
248
|
+
_signalCacheError("get");
|
|
236
249
|
return null;
|
|
237
250
|
}
|
|
238
251
|
}
|
|
@@ -241,7 +254,7 @@ function create(opts) {
|
|
|
241
254
|
try {
|
|
242
255
|
await cache.set(_scopedKey(key), state, { ttlMs: ttlMs });
|
|
243
256
|
} catch (_e) {
|
|
244
|
-
|
|
257
|
+
_signalCacheError("set");
|
|
245
258
|
}
|
|
246
259
|
}
|
|
247
260
|
|
|
@@ -249,7 +262,7 @@ function create(opts) {
|
|
|
249
262
|
try {
|
|
250
263
|
await cache.del(_scopedKey(key));
|
|
251
264
|
} catch (_e) {
|
|
252
|
-
|
|
265
|
+
_signalCacheError("del");
|
|
253
266
|
}
|
|
254
267
|
}
|
|
255
268
|
|
package/lib/auth/oauth.js
CHANGED
|
@@ -482,6 +482,20 @@ function create(opts) {
|
|
|
482
482
|
throw new OAuthError("auth-oauth/no-verifier",
|
|
483
483
|
"exchangeCode: opts.verifier is required when PKCE is on (default)");
|
|
484
484
|
}
|
|
485
|
+
// Nonce enforcement on OIDC paths. authorizationUrl() always
|
|
486
|
+
// emits a nonce when isOidc; if the operator forgot to thread it
|
|
487
|
+
// through to exchangeCode, _normalizeTokens silently skipped the
|
|
488
|
+
// nonce check on the ID token and a captured token from another
|
|
489
|
+
// browser session could be replayed without detection. Throw
|
|
490
|
+
// loudly so the operator sees the bug at config time, not at
|
|
491
|
+
// first-replay-attempt time.
|
|
492
|
+
if (isOidc && eopts.nonce === undefined && eopts.skipNonceCheck !== true) {
|
|
493
|
+
throw new OAuthError("auth-oauth/no-nonce",
|
|
494
|
+
"exchangeCode: nonce is required on OIDC flows. Pass the " +
|
|
495
|
+
"value returned from authorizationUrl() through to exchangeCode " +
|
|
496
|
+
"({ code, state, verifier, nonce }). Operators with a deliberate " +
|
|
497
|
+
"no-nonce flow must pass `skipNonceCheck: true` (audited reason).");
|
|
498
|
+
}
|
|
485
499
|
var endpoint = await _resolveEndpoint("tokenEndpoint");
|
|
486
500
|
var body = new URLSearchParams();
|
|
487
501
|
body.set("grant_type", "authorization_code");
|
|
@@ -492,7 +506,7 @@ function create(opts) {
|
|
|
492
506
|
if (eopts.verifier) body.set("code_verifier", eopts.verifier);
|
|
493
507
|
|
|
494
508
|
var tokens = await _postForm(endpoint, body);
|
|
495
|
-
return await _normalizeTokens(tokens, { nonce: eopts.nonce });
|
|
509
|
+
return await _normalizeTokens(tokens, { nonce: eopts.nonce, skipNonceCheck: eopts.skipNonceCheck });
|
|
496
510
|
}
|
|
497
511
|
|
|
498
512
|
async function refreshAccessToken(refreshToken) {
|
package/lib/auth/password.js
CHANGED
|
@@ -448,20 +448,40 @@ function policy(opts) {
|
|
|
448
448
|
}
|
|
449
449
|
var bodyText = Buffer.isBuffer(resp.body) ? resp.body.toString("utf8") : String(resp.body);
|
|
450
450
|
var lines = bodyText.split(/\r?\n/);
|
|
451
|
+
var goodLines = 0;
|
|
452
|
+
var badLines = 0;
|
|
451
453
|
for (var li = 0; li < lines.length; li++) {
|
|
452
454
|
var line = lines[li].trim();
|
|
453
455
|
if (line.length === 0) continue;
|
|
454
456
|
var colon = line.indexOf(":");
|
|
455
|
-
if (colon < 0) continue;
|
|
457
|
+
if (colon < 0) { badLines += 1; continue; }
|
|
456
458
|
var hashSuffix = line.slice(0, colon).toUpperCase();
|
|
457
459
|
var count = parseInt(line.slice(colon + 1), 10);
|
|
460
|
+
if (!isFinite(count)) { badLines += 1; continue; }
|
|
461
|
+
goodLines += 1;
|
|
458
462
|
if (timingSafeEqual(Buffer.from(hashSuffix, "utf8"), Buffer.from(suffix, "utf8")) &&
|
|
459
|
-
|
|
463
|
+
count >= p.breachThreshold) {
|
|
460
464
|
return _fail("breached",
|
|
461
465
|
"plaintext appears in HaveIBeenPwned with count " + count +
|
|
462
466
|
" (threshold " + p.breachThreshold + ")");
|
|
463
467
|
}
|
|
464
468
|
}
|
|
469
|
+
// If a hostile / poisoned mirror returned a response shaped like
|
|
470
|
+
// HIBP but with mostly-unparseable counts, the original loop
|
|
471
|
+
// skipped them silently and reported breachCheckCount=0 — i.e.
|
|
472
|
+
// the operator saw "looks fine" against a body that was never
|
|
473
|
+
// actually verifiable. When more than half the lines fail to
|
|
474
|
+
// parse, treat the response as unverifiable and apply the
|
|
475
|
+
// operator's fail-closed posture.
|
|
476
|
+
if (goodLines + badLines > 0 && badLines * 2 > goodLines) {
|
|
477
|
+
if (p.failClosed) {
|
|
478
|
+
return _fail("breach-check-failed",
|
|
479
|
+
"HIBP response was mostly-unparseable (good=" + goodLines +
|
|
480
|
+
", bad=" + badLines + ") — possible poisoned mirror");
|
|
481
|
+
}
|
|
482
|
+
return _ok({ breachCheckSkipped: true,
|
|
483
|
+
breachCheckSkipReason: "hibp-response-mostly-unparseable" });
|
|
484
|
+
}
|
|
465
485
|
return _ok({ breachCheckCount: 0 });
|
|
466
486
|
}
|
|
467
487
|
return _ok();
|
|
@@ -95,7 +95,7 @@ function create(opts) {
|
|
|
95
95
|
"issuer.create: activeKid \"" + activeKid + "\" is not in the keys array");
|
|
96
96
|
}
|
|
97
97
|
var defaultTtlMs = opts.defaultTtlMs || C.TIME.days(90);
|
|
98
|
-
var defaultHashAlg = opts.defaultHashAlg || "
|
|
98
|
+
var defaultHashAlg = opts.defaultHashAlg || "sha3-512";
|
|
99
99
|
var auditOn = opts.auditOn !== false;
|
|
100
100
|
|
|
101
101
|
var stats = {
|
|
@@ -137,7 +137,7 @@ function create(opts) {
|
|
|
137
137
|
claims: spec.claims || {},
|
|
138
138
|
selectivelyDisclosed: spec.selectivelyDisclosed || [],
|
|
139
139
|
issuerKey: key.privateKey,
|
|
140
|
-
algorithm: key.algorithm || "
|
|
140
|
+
algorithm: key.algorithm || "ML-DSA-87",
|
|
141
141
|
hashAlg: spec.hashAlg || defaultHashAlg,
|
|
142
142
|
ttlMs: spec.ttlMs || defaultTtlMs,
|
|
143
143
|
holderKey: spec.holderKey || null,
|
package/lib/auth/sd-jwt-vc.js
CHANGED
|
@@ -80,8 +80,13 @@ var SUPPORTED_HASH_ALGS = Object.freeze({
|
|
|
80
80
|
"sha3-512": "sha3-512",
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
// Defaults are PQC-first per the framework's hard rule §2 — operators
|
|
84
|
+
// who must interop with ES256-only verifiers today opt in via the
|
|
85
|
+
// `compatibilityProfile: "spec-default"` shape on the issuer/holder
|
|
86
|
+
// surfaces, OR pass `algorithm: "ES256"` + `hashAlg: "sha-256"`
|
|
87
|
+
// explicitly with an audited reason.
|
|
88
|
+
var DEFAULT_ALG = "ML-DSA-87";
|
|
89
|
+
var DEFAULT_HASH_ALG = "sha3-512";
|
|
85
90
|
|
|
86
91
|
function _b64uEncode(str) {
|
|
87
92
|
return Buffer.from(str, "utf8").toString("base64url");
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Step-up policy DSL — compose RFC 9470 step-up requirements as a
|
|
4
|
+
* fluent expression rather than a flat object. Useful for routes that
|
|
5
|
+
* need "ACR >= loa3 OR a fresh hardware-key auth within the last 5 min".
|
|
6
|
+
*
|
|
7
|
+
* var policy = b.auth.stepUp.policy
|
|
8
|
+
* .acr("loa3")
|
|
9
|
+
* .and(b.auth.stepUp.policy.maxAge(300))
|
|
10
|
+
* .or(
|
|
11
|
+
* b.auth.stepUp.policy.amr(["hwk", "pop"])
|
|
12
|
+
* .and(b.auth.stepUp.policy.maxAge(120))
|
|
13
|
+
* );
|
|
14
|
+
*
|
|
15
|
+
* var result = policy.evaluate(claims);
|
|
16
|
+
*
|
|
17
|
+
* var middleware = policy.middleware({ realm: "billing-api" });
|
|
18
|
+
*
|
|
19
|
+
* The policy compiles down to RFC 9470 challenges by extracting the
|
|
20
|
+
* outermost-feasible (acr, maxAge, amr, ...) tuple. When an operator's
|
|
21
|
+
* policy is genuinely or-of-and-of-..., the challenge emitted is the
|
|
22
|
+
* UNION of individually-required atoms (so the IdP knows what to ask
|
|
23
|
+
* for) plus an `error_description` hint that there are alternatives.
|
|
24
|
+
*
|
|
25
|
+
* The DSL is pure. All node types are immutable once constructed;
|
|
26
|
+
* chaining returns a new policy object.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
var lazyRequire = require("../lazy-require");
|
|
30
|
+
var validateOpts = require("../validate-opts");
|
|
31
|
+
var { AuthError } = require("../framework-error");
|
|
32
|
+
|
|
33
|
+
var stepUp = lazyRequire(function () { return require("./step-up"); });
|
|
34
|
+
var requireStepUp = lazyRequire(function () { return require("../middleware/require-step-up"); });
|
|
35
|
+
|
|
36
|
+
function _mkNode(spec) {
|
|
37
|
+
spec.evaluate = function (claims) {
|
|
38
|
+
return spec._run(claims);
|
|
39
|
+
};
|
|
40
|
+
spec.toRequirement = function () {
|
|
41
|
+
return spec._toReq();
|
|
42
|
+
};
|
|
43
|
+
spec.and = function (other) {
|
|
44
|
+
return _and(spec, other);
|
|
45
|
+
};
|
|
46
|
+
spec.or = function (other) {
|
|
47
|
+
return _or(spec, other);
|
|
48
|
+
};
|
|
49
|
+
spec.not = function () {
|
|
50
|
+
return _not(spec);
|
|
51
|
+
};
|
|
52
|
+
spec.middleware = function (opts) {
|
|
53
|
+
opts = opts || {};
|
|
54
|
+
return requireStepUp().create(Object.assign({}, opts, {
|
|
55
|
+
requirement: spec._toReq(),
|
|
56
|
+
}));
|
|
57
|
+
};
|
|
58
|
+
return Object.freeze(spec);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function acr(value) {
|
|
62
|
+
validateOpts.requireNonEmptyString(value, "policy.acr: value", AuthError, "auth-stepUp/bad-acr");
|
|
63
|
+
return _mkNode({
|
|
64
|
+
kind: "acr",
|
|
65
|
+
value: value,
|
|
66
|
+
_run: function (claims) {
|
|
67
|
+
var result = stepUp().evaluate({
|
|
68
|
+
claims: claims,
|
|
69
|
+
requirement: { acr: value },
|
|
70
|
+
});
|
|
71
|
+
return { ok: result.ok === true, atom: "acr:" + value, reason: result.reason || null };
|
|
72
|
+
},
|
|
73
|
+
_toReq: function () { return { acr: value }; },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function acrAny(values) {
|
|
78
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
79
|
+
throw new AuthError("auth-stepUp/bad-policy",
|
|
80
|
+
"policy.acrAny: values must be a non-empty array");
|
|
81
|
+
}
|
|
82
|
+
for (var i = 0; i < values.length; i += 1) {
|
|
83
|
+
validateOpts.requireNonEmptyString(values[i],
|
|
84
|
+
"policy.acrAny: values[" + i + "]", AuthError, "auth-stepUp/bad-acr");
|
|
85
|
+
}
|
|
86
|
+
var copy = values.slice();
|
|
87
|
+
return _mkNode({
|
|
88
|
+
kind: "acrAny",
|
|
89
|
+
values: copy,
|
|
90
|
+
_run: function (claims) {
|
|
91
|
+
var result = stepUp().evaluate({
|
|
92
|
+
claims: claims,
|
|
93
|
+
requirement: { acrValues: copy },
|
|
94
|
+
});
|
|
95
|
+
return { ok: result.ok === true, atom: "acrAny:" + copy.join(","), reason: result.reason || null };
|
|
96
|
+
},
|
|
97
|
+
_toReq: function () { return { acrValues: copy }; },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function amr(required) {
|
|
102
|
+
if (!Array.isArray(required) || required.length === 0) {
|
|
103
|
+
throw new AuthError("auth-stepUp/bad-policy",
|
|
104
|
+
"policy.amr: required must be a non-empty array");
|
|
105
|
+
}
|
|
106
|
+
for (var i = 0; i < required.length; i += 1) {
|
|
107
|
+
validateOpts.requireNonEmptyString(required[i],
|
|
108
|
+
"policy.amr: required[" + i + "]", AuthError, "auth-stepUp/bad-amr");
|
|
109
|
+
}
|
|
110
|
+
var copy = required.slice();
|
|
111
|
+
return _mkNode({
|
|
112
|
+
kind: "amr",
|
|
113
|
+
required: copy,
|
|
114
|
+
_run: function (claims) {
|
|
115
|
+
var result = stepUp().evaluate({
|
|
116
|
+
claims: claims,
|
|
117
|
+
requirement: { requiredAmr: copy },
|
|
118
|
+
});
|
|
119
|
+
return { ok: result.ok === true, atom: "amr:" + copy.join("+"), reason: result.reason || null };
|
|
120
|
+
},
|
|
121
|
+
_toReq: function () { return { requiredAmr: copy }; },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function phishingResistant() {
|
|
126
|
+
return _mkNode({
|
|
127
|
+
kind: "phishingResistant",
|
|
128
|
+
_run: function (claims) {
|
|
129
|
+
var result = stepUp().evaluate({
|
|
130
|
+
claims: claims,
|
|
131
|
+
requirement: { phishingResistant: true },
|
|
132
|
+
});
|
|
133
|
+
return { ok: result.ok === true, atom: "phr", reason: result.reason || null };
|
|
134
|
+
},
|
|
135
|
+
_toReq: function () { return { phishingResistant: true }; },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function maxAge(seconds) {
|
|
140
|
+
if (typeof seconds !== "number" || !isFinite(seconds) || seconds < 0) {
|
|
141
|
+
throw new AuthError("auth-stepUp/bad-policy",
|
|
142
|
+
"policy.maxAge: seconds must be a finite number >= 0 — got " +
|
|
143
|
+
JSON.stringify(seconds));
|
|
144
|
+
}
|
|
145
|
+
return _mkNode({
|
|
146
|
+
kind: "maxAge",
|
|
147
|
+
seconds: seconds,
|
|
148
|
+
_run: function (claims) {
|
|
149
|
+
var result = stepUp().evaluate({
|
|
150
|
+
claims: claims,
|
|
151
|
+
requirement: { maxAge: seconds },
|
|
152
|
+
});
|
|
153
|
+
return { ok: result.ok === true, atom: "maxAge:" + seconds, reason: result.reason || null };
|
|
154
|
+
},
|
|
155
|
+
_toReq: function () { return { maxAge: seconds }; },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function custom(name, fn) {
|
|
160
|
+
validateOpts.requireNonEmptyString(name, "policy.custom: name", AuthError, "auth-stepUp/bad-policy");
|
|
161
|
+
if (typeof fn !== "function") {
|
|
162
|
+
throw new AuthError("auth-stepUp/bad-policy",
|
|
163
|
+
"policy.custom: fn must be a function — got " + typeof fn);
|
|
164
|
+
}
|
|
165
|
+
return _mkNode({
|
|
166
|
+
kind: "custom",
|
|
167
|
+
name: name,
|
|
168
|
+
_run: function (claims) {
|
|
169
|
+
var ok = false;
|
|
170
|
+
try { ok = fn(claims) === true; } catch (_e) { ok = false; }
|
|
171
|
+
return { ok: ok, atom: "custom:" + name, reason: ok ? null : "custom predicate '" + name + "' returned false" };
|
|
172
|
+
},
|
|
173
|
+
_toReq: function () {
|
|
174
|
+
throw new AuthError("auth-stepUp/policy-no-challenge",
|
|
175
|
+
"policy.custom: cannot translate to RFC 9470 challenge — wrap in .or() with a translatable atom or use .middleware({ requirement: ... })");
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function _and(left, right) {
|
|
181
|
+
return _mkNode({
|
|
182
|
+
kind: "and",
|
|
183
|
+
left: left,
|
|
184
|
+
right: right,
|
|
185
|
+
_run: function (claims) {
|
|
186
|
+
var l = left._run(claims);
|
|
187
|
+
if (!l.ok) return { ok: false, atom: "and(" + l.atom + ")", reason: l.reason };
|
|
188
|
+
var r = right._run(claims);
|
|
189
|
+
if (!r.ok) return { ok: false, atom: "and(" + l.atom + "," + r.atom + ")", reason: r.reason };
|
|
190
|
+
return { ok: true, atom: "and(" + l.atom + "," + r.atom + ")", reason: null };
|
|
191
|
+
},
|
|
192
|
+
_toReq: function () {
|
|
193
|
+
var lr = left._toReq();
|
|
194
|
+
var rr = right._toReq();
|
|
195
|
+
return _mergeAnd(lr, rr);
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function _or(left, right) {
|
|
201
|
+
return _mkNode({
|
|
202
|
+
kind: "or",
|
|
203
|
+
left: left,
|
|
204
|
+
right: right,
|
|
205
|
+
_run: function (claims) {
|
|
206
|
+
var l = left._run(claims);
|
|
207
|
+
if (l.ok) return { ok: true, atom: "or(" + l.atom + ")", reason: null };
|
|
208
|
+
var r = right._run(claims);
|
|
209
|
+
if (r.ok) return { ok: true, atom: "or(" + r.atom + ")", reason: null };
|
|
210
|
+
return {
|
|
211
|
+
ok: false,
|
|
212
|
+
atom: "or(" + l.atom + "," + r.atom + ")",
|
|
213
|
+
reason: l.reason + " AND " + r.reason,
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
_toReq: function () {
|
|
217
|
+
// RFC 9470 doesn't support OR semantics in WWW-Authenticate. We
|
|
218
|
+
// pick the LEFT branch and emit its challenge — operator can
|
|
219
|
+
// override via .middleware({ requirement }).
|
|
220
|
+
return left._toReq();
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function _not(inner) {
|
|
226
|
+
return _mkNode({
|
|
227
|
+
kind: "not",
|
|
228
|
+
inner: inner,
|
|
229
|
+
_run: function (claims) {
|
|
230
|
+
var i = inner._run(claims);
|
|
231
|
+
return { ok: !i.ok, atom: "not(" + i.atom + ")", reason: i.ok ? "not(" + i.atom + ") matched" : null };
|
|
232
|
+
},
|
|
233
|
+
_toReq: function () {
|
|
234
|
+
throw new AuthError("auth-stepUp/policy-no-challenge",
|
|
235
|
+
"policy.not: cannot translate to RFC 9470 challenge");
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function _mergeAnd(a, b) {
|
|
241
|
+
var out = {};
|
|
242
|
+
if (a.acr || b.acr) {
|
|
243
|
+
if (a.acr && b.acr && a.acr !== b.acr) {
|
|
244
|
+
throw new AuthError("auth-stepUp/policy-conflict",
|
|
245
|
+
"policy.and: conflicting acr requirements " +
|
|
246
|
+
JSON.stringify(a.acr) + " and " + JSON.stringify(b.acr));
|
|
247
|
+
}
|
|
248
|
+
out.acr = a.acr || b.acr;
|
|
249
|
+
}
|
|
250
|
+
if (a.acrValues || b.acrValues) {
|
|
251
|
+
out.acrValues = (a.acrValues || []).concat(b.acrValues || []);
|
|
252
|
+
}
|
|
253
|
+
if (a.maxAge != null && b.maxAge != null) {
|
|
254
|
+
out.maxAge = Math.min(a.maxAge, b.maxAge); // tighter wins
|
|
255
|
+
} else if (a.maxAge != null) {
|
|
256
|
+
out.maxAge = a.maxAge;
|
|
257
|
+
} else if (b.maxAge != null) {
|
|
258
|
+
out.maxAge = b.maxAge;
|
|
259
|
+
}
|
|
260
|
+
if (a.requiredAmr || b.requiredAmr) {
|
|
261
|
+
var combined = (a.requiredAmr || []).concat(b.requiredAmr || []);
|
|
262
|
+
var seen = Object.create(null);
|
|
263
|
+
out.requiredAmr = [];
|
|
264
|
+
for (var i = 0; i < combined.length; i += 1) {
|
|
265
|
+
if (!seen[combined[i]]) {
|
|
266
|
+
seen[combined[i]] = true;
|
|
267
|
+
out.requiredAmr.push(combined[i]);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (a.phishingResistant === true || b.phishingResistant === true) {
|
|
272
|
+
out.phishingResistant = true;
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ---- Common preset policies operators reach for ----
|
|
278
|
+
|
|
279
|
+
var C = require("../constants");
|
|
280
|
+
var SEC_5_MIN = C.TIME.minutes(5) / C.TIME.seconds(1);
|
|
281
|
+
var SEC_2_MIN = C.TIME.minutes(2) / C.TIME.seconds(1);
|
|
282
|
+
var SEC_15_MIN = C.TIME.minutes(15) / C.TIME.seconds(1);
|
|
283
|
+
var SEC_1_MIN = C.TIME.minutes(1) / C.TIME.seconds(1);
|
|
284
|
+
|
|
285
|
+
var PRESETS = {
|
|
286
|
+
// Sensitive write: ACR >= loa2 + auth_time within 5 min
|
|
287
|
+
sensitiveWrite: function () {
|
|
288
|
+
return acr("loa2").and(maxAge(SEC_5_MIN));
|
|
289
|
+
},
|
|
290
|
+
// Admin bulk: ACR >= loa3 + max_age 5 min + phishing-resistant
|
|
291
|
+
adminBulk: function () {
|
|
292
|
+
return acr("loa3").and(maxAge(SEC_5_MIN)).and(phishingResistant());
|
|
293
|
+
},
|
|
294
|
+
// Financial: phr-class hardware + max_age 2 min
|
|
295
|
+
financial: function () {
|
|
296
|
+
return acr("loa3").and(amr(["hwk"])).and(maxAge(SEC_2_MIN));
|
|
297
|
+
},
|
|
298
|
+
// PHI read: ACR >= loa2 + max_age 15 min
|
|
299
|
+
phiRead: function () {
|
|
300
|
+
return acr("loa2").and(maxAge(SEC_15_MIN));
|
|
301
|
+
},
|
|
302
|
+
// PHI write: ACR >= loa3 + max_age 5 min + phishing-resistant
|
|
303
|
+
phiWrite: function () {
|
|
304
|
+
return acr("loa3").and(maxAge(SEC_5_MIN)).and(phishingResistant());
|
|
305
|
+
},
|
|
306
|
+
// Account-recovery: phishing-resistant + max_age 1 min
|
|
307
|
+
accountRecovery: function () {
|
|
308
|
+
return phishingResistant().and(maxAge(SEC_1_MIN));
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
function preset(name) {
|
|
313
|
+
if (!Object.prototype.hasOwnProperty.call(PRESETS, name)) {
|
|
314
|
+
throw new AuthError("auth-stepUp/bad-preset",
|
|
315
|
+
"policy.preset: unknown preset " + JSON.stringify(name) +
|
|
316
|
+
" — valid presets: " + Object.keys(PRESETS).join(", "));
|
|
317
|
+
}
|
|
318
|
+
return PRESETS[name]();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function listPresets() {
|
|
322
|
+
return Object.keys(PRESETS).slice();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
module.exports = {
|
|
326
|
+
acr: acr,
|
|
327
|
+
acrAny: acrAny,
|
|
328
|
+
amr: amr,
|
|
329
|
+
phishingResistant: phishingResistant,
|
|
330
|
+
maxAge: maxAge,
|
|
331
|
+
custom: custom,
|
|
332
|
+
preset: preset,
|
|
333
|
+
listPresets: listPresets,
|
|
334
|
+
PRESETS: PRESETS,
|
|
335
|
+
};
|