@blamejs/core 0.7.106 → 0.8.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/CHANGELOG.md +19 -1
- package/NOTICE +17 -1
- package/README.md +4 -3
- package/index.js +16 -0
- package/lib/asyncapi-bindings.js +160 -0
- package/lib/asyncapi-traits.js +143 -0
- package/lib/asyncapi.js +531 -0
- package/lib/audit.js +6 -0
- 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/sd-jwt-vc-disclosure.js +95 -0
- package/lib/auth/sd-jwt-vc-holder.js +203 -0
- package/lib/auth/sd-jwt-vc-issuer.js +197 -0
- package/lib/auth/sd-jwt-vc.js +526 -0
- package/lib/auth/step-up-policy.js +335 -0
- package/lib/auth/step-up.js +445 -0
- package/lib/compliance-ai-act-logging.js +186 -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 +2 -0
- package/lib/crypto.js +32 -0
- 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/inbox.js +367 -0
- package/lib/mail-arc-sign.js +372 -0
- package/lib/mail-auth.js +2 -0
- package/lib/middleware/ai-act-disclosure.js +166 -0
- package/lib/middleware/asyncapi-serve.js +136 -0
- package/lib/middleware/flag-context.js +76 -0
- package/lib/middleware/index.js +15 -0
- package/lib/middleware/openapi-serve.js +143 -0
- package/lib/middleware/require-step-up.js +186 -0
- 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/pqc-software.js +195 -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 +829 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* flag-context middleware — extracts an OpenFeature evaluation context
|
|
4
|
+
* onto the request so downstream handlers and multiple flag clients
|
|
5
|
+
* can read a consistent context without re-deriving it per call.
|
|
6
|
+
*
|
|
7
|
+
* var attachCtx = b.middleware.flagContext({
|
|
8
|
+
* userKey: "x-user-id", // header to pull targetingKey from
|
|
9
|
+
* extractAttributes: function (req) { // operator-supplied augmentation
|
|
10
|
+
* return { tenantId: req.tenantId, environment: process.env.NODE_ENV };
|
|
11
|
+
* },
|
|
12
|
+
* });
|
|
13
|
+
* router.use(attachCtx);
|
|
14
|
+
*
|
|
15
|
+
* // Downstream:
|
|
16
|
+
* var ctx = req.flagCtx; // readonly Frozen object
|
|
17
|
+
* var enabled = b.flagClient.getBoolean("foo", ctx);
|
|
18
|
+
*
|
|
19
|
+
* The middleware does NOT evaluate flags — it only constructs the
|
|
20
|
+
* context. Pair with `flag.middleware()` for the request-attached
|
|
21
|
+
* convenience accessor; or pass `req.flagCtx` directly to a flag
|
|
22
|
+
* client method for a more decoupled shape (e.g. when several flag
|
|
23
|
+
* clients with different providers share the same context).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
var validateOpts = require("../validate-opts");
|
|
27
|
+
var lazyRequire = require("../lazy-require");
|
|
28
|
+
var { defineClass } = require("../framework-error");
|
|
29
|
+
var FlagError = defineClass("FlagError", { alwaysPermanent: true });
|
|
30
|
+
|
|
31
|
+
var contextMod = lazyRequire(function () { return require("../flag-evaluation-context"); });
|
|
32
|
+
|
|
33
|
+
function create(opts) {
|
|
34
|
+
opts = opts || {};
|
|
35
|
+
validateOpts(opts, [
|
|
36
|
+
"userKey", "userKeyHeader", "extractAttributes", "tenantKeyHeader",
|
|
37
|
+
], "middleware.flagContext");
|
|
38
|
+
if (opts.extractAttributes != null && typeof opts.extractAttributes !== "function") {
|
|
39
|
+
throw new FlagError("flag/bad-opt",
|
|
40
|
+
"flagContext: extractAttributes must be a function");
|
|
41
|
+
}
|
|
42
|
+
var userKeyHeader = (typeof opts.userKeyHeader === "string" && opts.userKeyHeader.length > 0)
|
|
43
|
+
? opts.userKeyHeader.toLowerCase()
|
|
44
|
+
: null;
|
|
45
|
+
var tenantKeyHeader = (typeof opts.tenantKeyHeader === "string" && opts.tenantKeyHeader.length > 0)
|
|
46
|
+
? opts.tenantKeyHeader.toLowerCase()
|
|
47
|
+
: null;
|
|
48
|
+
var explicitUserKey = (typeof opts.userKey === "string" && opts.userKey.length > 0)
|
|
49
|
+
? opts.userKey
|
|
50
|
+
: null;
|
|
51
|
+
|
|
52
|
+
return function flagContextMiddleware(req, res, next) {
|
|
53
|
+
var headers = req.headers || {};
|
|
54
|
+
var headerKey = userKeyHeader && typeof headers[userKeyHeader] === "string"
|
|
55
|
+
? headers[userKeyHeader]
|
|
56
|
+
: null;
|
|
57
|
+
var fromReqOpts = {};
|
|
58
|
+
if (explicitUserKey) fromReqOpts.userKey = explicitUserKey;
|
|
59
|
+
else if (headerKey) fromReqOpts.userKey = headerKey;
|
|
60
|
+
var augment = {};
|
|
61
|
+
if (typeof opts.extractAttributes === "function") {
|
|
62
|
+
try {
|
|
63
|
+
var extra = opts.extractAttributes(req);
|
|
64
|
+
if (extra && typeof extra === "object") augment = extra;
|
|
65
|
+
} catch (_e) { /* drop-silent on extraction error */ }
|
|
66
|
+
}
|
|
67
|
+
if (tenantKeyHeader && typeof headers[tenantKeyHeader] === "string") {
|
|
68
|
+
augment.tenantId = headers[tenantKeyHeader];
|
|
69
|
+
}
|
|
70
|
+
fromReqOpts.extra = augment;
|
|
71
|
+
req.flagCtx = contextMod().fromRequest(req, fromReqOpts);
|
|
72
|
+
return next();
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { create: create };
|
package/lib/middleware/index.js
CHANGED
|
@@ -16,7 +16,11 @@
|
|
|
16
16
|
* 6. (your auth + business middleware + routes)
|
|
17
17
|
* 7. errorHandler — must be LAST so it catches everything that throws
|
|
18
18
|
*/
|
|
19
|
+
var aiActDisclosure = require("./ai-act-disclosure");
|
|
19
20
|
var apiEncrypt = require("./api-encrypt");
|
|
21
|
+
var openapiServe = require("./openapi-serve");
|
|
22
|
+
var asyncapiServe = require("./asyncapi-serve");
|
|
23
|
+
var flagContext = require("./flag-context");
|
|
20
24
|
var assetlinks = require("./assetlinks");
|
|
21
25
|
var attachUser = require("./attach-user");
|
|
22
26
|
var bearerAuth = require("./bearer-auth");
|
|
@@ -43,6 +47,7 @@ var requireAal = require("./require-aal");
|
|
|
43
47
|
var requireAuth = require("./require-auth");
|
|
44
48
|
var requireContentType = require("./require-content-type");
|
|
45
49
|
var requireMethods = require("./require-methods");
|
|
50
|
+
var requireStepUp = require("./require-step-up");
|
|
46
51
|
var securityHeaders = require("./security-headers");
|
|
47
52
|
var securityTxt = require("./security-txt");
|
|
48
53
|
var spanHttpServer = require("./span-http-server");
|
|
@@ -65,6 +70,7 @@ module.exports = {
|
|
|
65
70
|
requireAuth: requireAuth.create,
|
|
66
71
|
requireContentType: requireContentType.create,
|
|
67
72
|
requireMethods: requireMethods.create,
|
|
73
|
+
requireStepUp: requireStepUp.create,
|
|
68
74
|
csrfProtect: csrfProtect.create,
|
|
69
75
|
fetchMetadata: fetchMetadata.create,
|
|
70
76
|
gpc: gpc.create,
|
|
@@ -78,6 +84,10 @@ module.exports = {
|
|
|
78
84
|
sse: sse.create,
|
|
79
85
|
requestLog: requestLog.create,
|
|
80
86
|
apiEncrypt: apiEncrypt,
|
|
87
|
+
aiActDisclosure: aiActDisclosure.create,
|
|
88
|
+
openapiServe: openapiServe.create,
|
|
89
|
+
asyncapiServe: asyncapiServe.create,
|
|
90
|
+
flagContext: flagContext.create,
|
|
81
91
|
assetlinks: assetlinks.create,
|
|
82
92
|
dbRoleFor: dbRoleFor.create,
|
|
83
93
|
dpop: dpop.create,
|
|
@@ -103,6 +113,7 @@ module.exports = {
|
|
|
103
113
|
requireAuth: requireAuth,
|
|
104
114
|
requireContentType: requireContentType,
|
|
105
115
|
requireMethods: requireMethods,
|
|
116
|
+
requireStepUp: requireStepUp,
|
|
106
117
|
csrfProtect: csrfProtect,
|
|
107
118
|
fetchMetadata: fetchMetadata,
|
|
108
119
|
bodyParser: bodyParser,
|
|
@@ -113,6 +124,10 @@ module.exports = {
|
|
|
113
124
|
sse: sse,
|
|
114
125
|
requestLog: requestLog,
|
|
115
126
|
apiEncrypt: apiEncrypt,
|
|
127
|
+
aiActDisclosure: aiActDisclosure,
|
|
128
|
+
openapiServe: openapiServe,
|
|
129
|
+
asyncapiServe: asyncapiServe,
|
|
130
|
+
flagContext: flagContext,
|
|
116
131
|
assetlinks: assetlinks,
|
|
117
132
|
dbRoleFor: dbRoleFor,
|
|
118
133
|
dpop: dpop,
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* openapi-serve middleware — expose an OpenAPI 3.1 document as a
|
|
4
|
+
* request-time JSON / YAML resource at a single mount point.
|
|
5
|
+
*
|
|
6
|
+
* var openapi = b.openapi.create({ ... });
|
|
7
|
+
* ...add paths / schemas / security...
|
|
8
|
+
*
|
|
9
|
+
* var serve = b.middleware.openapiServe({
|
|
10
|
+
* document: openapi,
|
|
11
|
+
* pathJson: "/openapi.json",
|
|
12
|
+
* pathYaml: "/openapi.yaml",
|
|
13
|
+
* pretty: true,
|
|
14
|
+
* cacheControl: "public, max-age=300",
|
|
15
|
+
* });
|
|
16
|
+
* router.use(serve);
|
|
17
|
+
*
|
|
18
|
+
* The middleware ONLY responds to GET requests for the configured
|
|
19
|
+
* paths; everything else passes to next() unchanged. ETag is computed
|
|
20
|
+
* from the JSON-string SHA3-512 to allow conditional GET.
|
|
21
|
+
*
|
|
22
|
+
* If `accessControl: "public"` (the default), the middleware emits
|
|
23
|
+
* `Access-Control-Allow-Origin: *` so external doc tooling can fetch.
|
|
24
|
+
* For internal-only docs operators set `accessControl: "same-origin"`
|
|
25
|
+
* which omits the CORS header.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
var nodeCrypto = require("crypto");
|
|
29
|
+
var validateOpts = require("../validate-opts");
|
|
30
|
+
var lazyRequire = require("../lazy-require");
|
|
31
|
+
var { defineClass } = require("../framework-error");
|
|
32
|
+
var OpenApiError = defineClass("OpenApiError", { alwaysPermanent: true });
|
|
33
|
+
|
|
34
|
+
var openapiYaml = lazyRequire(function () { return require("../openapi-yaml"); });
|
|
35
|
+
var audit = lazyRequire(function () { return require("../audit"); });
|
|
36
|
+
|
|
37
|
+
function create(opts) {
|
|
38
|
+
opts = opts || {};
|
|
39
|
+
validateOpts(opts, [
|
|
40
|
+
"document", "pathJson", "pathYaml", "pretty",
|
|
41
|
+
"cacheControl", "accessControl", "audit",
|
|
42
|
+
], "middleware.openapiServe");
|
|
43
|
+
if (!opts.document || typeof opts.document.toJson !== "function") {
|
|
44
|
+
throw new OpenApiError("openapi/bad-document",
|
|
45
|
+
"openapiServe: document must be a builder created via b.openapi.create()");
|
|
46
|
+
}
|
|
47
|
+
var pathJson = opts.pathJson || "/openapi.json";
|
|
48
|
+
var pathYaml = opts.pathYaml || "/openapi.yaml";
|
|
49
|
+
var pretty = opts.pretty === true ? 2 : 0;
|
|
50
|
+
var cacheControl = (typeof opts.cacheControl === "string" && opts.cacheControl.length > 0)
|
|
51
|
+
? opts.cacheControl : "public, max-age=300";
|
|
52
|
+
var accessControl = opts.accessControl || "public";
|
|
53
|
+
var auditOn = opts.audit !== false;
|
|
54
|
+
|
|
55
|
+
if (typeof pathJson !== "string" || pathJson.charAt(0) !== "/") {
|
|
56
|
+
throw new OpenApiError("openapi/bad-path",
|
|
57
|
+
"openapiServe: pathJson must start with '/' - got " + JSON.stringify(pathJson));
|
|
58
|
+
}
|
|
59
|
+
if (typeof pathYaml !== "string" || pathYaml.charAt(0) !== "/") {
|
|
60
|
+
throw new OpenApiError("openapi/bad-path",
|
|
61
|
+
"openapiServe: pathYaml must start with '/' - got " + JSON.stringify(pathYaml));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
var cachedDoc = null;
|
|
65
|
+
var cachedJsonStr = null;
|
|
66
|
+
var cachedYamlStr = null;
|
|
67
|
+
var cachedJsonEtag = null;
|
|
68
|
+
var cachedYamlEtag = null;
|
|
69
|
+
|
|
70
|
+
function _rebuild() {
|
|
71
|
+
cachedDoc = opts.document.toJson();
|
|
72
|
+
cachedJsonStr = JSON.stringify(cachedDoc, null, pretty);
|
|
73
|
+
cachedYamlStr = openapiYaml().toYaml(cachedDoc);
|
|
74
|
+
cachedJsonEtag = '"' + nodeCrypto.createHash("sha3-512").update(cachedJsonStr).digest("base64url").slice(0, 24) + '"';
|
|
75
|
+
cachedYamlEtag = '"' + nodeCrypto.createHash("sha3-512").update(cachedYamlStr).digest("base64url").slice(0, 24) + '"';
|
|
76
|
+
}
|
|
77
|
+
_rebuild();
|
|
78
|
+
|
|
79
|
+
function _writeBody(req, res, body, etag, contentType) {
|
|
80
|
+
var requestEtag = (req.headers && req.headers["if-none-match"]) || null;
|
|
81
|
+
if (requestEtag && requestEtag === etag) {
|
|
82
|
+
res.writeHead(304, { "ETag": etag, "Cache-Control": cacheControl }); // allow:raw-byte-literal — HTTP 304
|
|
83
|
+
res.end();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
var headers = {
|
|
87
|
+
"Content-Type": contentType,
|
|
88
|
+
"Content-Length": Buffer.byteLength(body),
|
|
89
|
+
"Cache-Control": cacheControl,
|
|
90
|
+
"ETag": etag,
|
|
91
|
+
};
|
|
92
|
+
if (accessControl === "public") {
|
|
93
|
+
headers["Access-Control-Allow-Origin"] = "*";
|
|
94
|
+
}
|
|
95
|
+
res.writeHead(200, headers); // allow:raw-byte-literal — HTTP 200
|
|
96
|
+
res.end(body);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
var mw = function (req, res, next) {
|
|
100
|
+
if (typeof res.writeHead !== "function") return next();
|
|
101
|
+
var method = (req.method || "GET").toUpperCase();
|
|
102
|
+
if (method !== "GET" && method !== "HEAD") return next();
|
|
103
|
+
var pathname = req.pathname;
|
|
104
|
+
if (typeof pathname !== "string") {
|
|
105
|
+
var url = req.url || "";
|
|
106
|
+
var qIdx = url.indexOf("?");
|
|
107
|
+
pathname = qIdx === -1 ? url : url.slice(0, qIdx);
|
|
108
|
+
}
|
|
109
|
+
if (pathname === pathJson) {
|
|
110
|
+
_writeBody(req, res, cachedJsonStr, cachedJsonEtag, "application/json; charset=utf-8");
|
|
111
|
+
if (auditOn) {
|
|
112
|
+
try {
|
|
113
|
+
audit().safeEmit({
|
|
114
|
+
action: "openapi.document.served",
|
|
115
|
+
outcome: "success",
|
|
116
|
+
actor: null,
|
|
117
|
+
metadata: { format: "json", path: pathname, bytes: cachedJsonStr.length },
|
|
118
|
+
});
|
|
119
|
+
} catch (_e) { /* drop-silent */ }
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (pathname === pathYaml) {
|
|
124
|
+
_writeBody(req, res, cachedYamlStr, cachedYamlEtag, "application/yaml; charset=utf-8");
|
|
125
|
+
if (auditOn) {
|
|
126
|
+
try {
|
|
127
|
+
audit().safeEmit({
|
|
128
|
+
action: "openapi.document.served",
|
|
129
|
+
outcome: "success",
|
|
130
|
+
actor: null,
|
|
131
|
+
metadata: { format: "yaml", path: pathname, bytes: cachedYamlStr.length },
|
|
132
|
+
});
|
|
133
|
+
} catch (_e) { /* drop-silent */ }
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
return next();
|
|
138
|
+
};
|
|
139
|
+
mw.forceRebuild = _rebuild;
|
|
140
|
+
return mw;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = { create: create };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* require-step-up middleware — gate routes per RFC 9470 OAuth 2.0
|
|
4
|
+
* Step-Up Authentication Challenge.
|
|
5
|
+
*
|
|
6
|
+
* Mounted AFTER attachUser / bearerAuth so the request carries the
|
|
7
|
+
* already-verified token claims.
|
|
8
|
+
*
|
|
9
|
+
* var sensitiveStepUp = b.middleware.requireStepUp({
|
|
10
|
+
* requirement: { acr: "high", maxAge: 300 },
|
|
11
|
+
* realm: "billing-api",
|
|
12
|
+
* });
|
|
13
|
+
* router.use("/billing/transfer", sensitiveStepUp);
|
|
14
|
+
*
|
|
15
|
+
* Failure shape (per RFC 9470 §3):
|
|
16
|
+
* 401 Unauthorized
|
|
17
|
+
* WWW-Authenticate: Bearer error="insufficient_user_authentication",
|
|
18
|
+
* error_description="...", acr_values="high", max_age="300"
|
|
19
|
+
* Content-Type: application/json
|
|
20
|
+
* { "error": "insufficient_user_authentication", "error_description": "..." }
|
|
21
|
+
*
|
|
22
|
+
* Operators with their own elevation grants pass `acceptGrant: true`
|
|
23
|
+
* and `grantHeader: "X-Step-Up-Grant"` (default) — the middleware
|
|
24
|
+
* checks for a valid b.auth.stepUp.grant token before evaluating the
|
|
25
|
+
* normal claims-based requirement, so a multi-step flow doesn't get
|
|
26
|
+
* step-up-prompted on every action.
|
|
27
|
+
*
|
|
28
|
+
* Options:
|
|
29
|
+
* {
|
|
30
|
+
* requirement: { acr / acrValues / maxAge / requiredAmr / phishingResistant },
|
|
31
|
+
* getClaims: function(req) { return req.user.claims; },
|
|
32
|
+
* realm: "api",
|
|
33
|
+
* audit: true,
|
|
34
|
+
* acceptGrant: true, // default
|
|
35
|
+
* grantHeader: "X-Step-Up-Grant", // default
|
|
36
|
+
* grantScope: null, // narrow grant by scope
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* NEVER weaken the security default to fix a broken caller. Operators
|
|
40
|
+
* configure their IdP to emit `acr` / `auth_time` / `amr` correctly;
|
|
41
|
+
* the middleware does not silently default these to "good enough" on a
|
|
42
|
+
* missing claim.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
var lazyRequire = require("../lazy-require");
|
|
46
|
+
var validateOpts = require("../validate-opts");
|
|
47
|
+
var requestHelpers = require("../request-helpers");
|
|
48
|
+
var { AuthError } = require("../framework-error");
|
|
49
|
+
|
|
50
|
+
var stepUp = lazyRequire(function () { return require("../auth/step-up"); });
|
|
51
|
+
var elevation = lazyRequire(function () { return require("../auth/elevation-grant"); });
|
|
52
|
+
var audit = lazyRequire(function () { return require("../audit"); });
|
|
53
|
+
|
|
54
|
+
var DEFAULT_GRANT_HEADER = "x-step-up-grant";
|
|
55
|
+
|
|
56
|
+
function _defaultGetClaims(req) {
|
|
57
|
+
if (!req || typeof req !== "object") return null;
|
|
58
|
+
if (req.user && req.user.claims && typeof req.user.claims === "object") {
|
|
59
|
+
return req.user.claims;
|
|
60
|
+
}
|
|
61
|
+
if (req.user && typeof req.user === "object") {
|
|
62
|
+
return req.user;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function _writeChallenge(res, challenge, body, statusCode) {
|
|
68
|
+
if (res.headersSent) return;
|
|
69
|
+
var json = JSON.stringify(body);
|
|
70
|
+
res.writeHead(statusCode, { // allow:raw-byte-literal — HTTP status passthrough
|
|
71
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
72
|
+
"Content-Length": Buffer.byteLength(json),
|
|
73
|
+
"WWW-Authenticate": challenge,
|
|
74
|
+
});
|
|
75
|
+
res.end(json);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function create(opts) {
|
|
79
|
+
opts = opts || {};
|
|
80
|
+
validateOpts(opts, [
|
|
81
|
+
"requirement", "getClaims", "realm", "audit",
|
|
82
|
+
"acceptGrant", "grantHeader", "grantScope", "errorDescription",
|
|
83
|
+
], "middleware.requireStepUp");
|
|
84
|
+
|
|
85
|
+
if (!opts.requirement || typeof opts.requirement !== "object") {
|
|
86
|
+
throw new AuthError("auth-stepUp/bad-requirement",
|
|
87
|
+
"middleware.requireStepUp: opts.requirement must be an object");
|
|
88
|
+
}
|
|
89
|
+
validateOpts.optionalFunction(opts.getClaims,
|
|
90
|
+
"middleware.requireStepUp: getClaims", AuthError, "auth-stepUp/bad-opt");
|
|
91
|
+
|
|
92
|
+
var realm = (typeof opts.realm === "string" && opts.realm.length > 0)
|
|
93
|
+
? opts.realm : "api";
|
|
94
|
+
var auditOn = opts.audit !== false;
|
|
95
|
+
var getClaims = (typeof opts.getClaims === "function")
|
|
96
|
+
? opts.getClaims : _defaultGetClaims;
|
|
97
|
+
var acceptGrant = opts.acceptGrant !== false;
|
|
98
|
+
var grantHeader = (typeof opts.grantHeader === "string" && opts.grantHeader.length > 0)
|
|
99
|
+
? opts.grantHeader.toLowerCase() : DEFAULT_GRANT_HEADER;
|
|
100
|
+
var grantScope = (typeof opts.grantScope === "string" && opts.grantScope.length > 0)
|
|
101
|
+
? opts.grantScope : null;
|
|
102
|
+
var errorDesc = (typeof opts.errorDescription === "string" && opts.errorDescription.length > 0)
|
|
103
|
+
? opts.errorDescription : null;
|
|
104
|
+
|
|
105
|
+
// Pre-validate the requirement so operator typos surface at boot, not
|
|
106
|
+
// on the first hot-path request.
|
|
107
|
+
var probe = stepUp().evaluate({ claims: { acr: "0" }, requirement: opts.requirement });
|
|
108
|
+
if (probe.error === "bad_requirement" || probe.error === "unknown_acr") {
|
|
109
|
+
throw new AuthError("auth-stepUp/bad-requirement",
|
|
110
|
+
"middleware.requireStepUp: " + (probe.reason || probe.error));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return function requireStepUpMiddleware(req, res, next) {
|
|
114
|
+
var headers = req.headers || {};
|
|
115
|
+
|
|
116
|
+
// Path 1: operator-issued elevation grant — short-circuit success.
|
|
117
|
+
if (acceptGrant) {
|
|
118
|
+
var grantToken = headers[grantHeader] || null;
|
|
119
|
+
if (typeof grantToken === "string" && grantToken.length > 0) {
|
|
120
|
+
var verifyOpts = {};
|
|
121
|
+
if (grantScope) verifyOpts.scope = grantScope;
|
|
122
|
+
var grantResult = elevation().verify(grantToken, verifyOpts);
|
|
123
|
+
if (grantResult.ok) {
|
|
124
|
+
if (auditOn) {
|
|
125
|
+
try {
|
|
126
|
+
audit().safeEmit({
|
|
127
|
+
action: "auth.stepup.satisfied",
|
|
128
|
+
outcome: "success",
|
|
129
|
+
actor: { userId: grantResult.payload.sub,
|
|
130
|
+
clientIp: requestHelpers.clientIp(req) },
|
|
131
|
+
metadata: {
|
|
132
|
+
reason: "grant",
|
|
133
|
+
jti: grantResult.payload.jti || null,
|
|
134
|
+
scope: grantResult.payload.scope,
|
|
135
|
+
route: req.url || null,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
} catch (_e) { /* drop-silent */ }
|
|
139
|
+
}
|
|
140
|
+
if (req.user) req.user.stepUp = { byGrant: true, payload: grantResult.payload };
|
|
141
|
+
return next();
|
|
142
|
+
}
|
|
143
|
+
// Invalid grant — fall through to claims-based path; emit signal.
|
|
144
|
+
if (auditOn) {
|
|
145
|
+
try {
|
|
146
|
+
audit().safeEmit({
|
|
147
|
+
action: "auth.stepup.grant.rejected",
|
|
148
|
+
outcome: "denied",
|
|
149
|
+
actor: { clientIp: requestHelpers.clientIp(req) },
|
|
150
|
+
metadata: { error: grantResult.error, reason: grantResult.reason },
|
|
151
|
+
});
|
|
152
|
+
} catch (_e) { /* drop-silent */ }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Path 2: claims-based evaluation.
|
|
158
|
+
var claims = getClaims(req);
|
|
159
|
+
var result = stepUp().evaluate({ claims: claims, requirement: opts.requirement });
|
|
160
|
+
|
|
161
|
+
if (result.ok) {
|
|
162
|
+
if (auditOn) stepUp().emitAuditSatisfied("requireStepUp", opts.requirement, result.presented, req);
|
|
163
|
+
if (req.user) req.user.stepUp = { byClaims: true, presented: result.presented };
|
|
164
|
+
return next();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (auditOn) stepUp().emitAuditRequired("requireStepUp", opts.requirement, result.presented, req);
|
|
168
|
+
|
|
169
|
+
var challenge = stepUp().buildChallenge({
|
|
170
|
+
requirement: opts.requirement,
|
|
171
|
+
realm: realm,
|
|
172
|
+
error: stepUp().INSUFFICIENT_USER_AUTHENTICATION,
|
|
173
|
+
errorDescription: errorDesc || undefined,
|
|
174
|
+
});
|
|
175
|
+
_writeChallenge(res,
|
|
176
|
+
challenge,
|
|
177
|
+
{
|
|
178
|
+
error: stepUp().INSUFFICIENT_USER_AUTHENTICATION,
|
|
179
|
+
error_description: errorDesc || "A higher level of authentication is required",
|
|
180
|
+
},
|
|
181
|
+
401 // allow:raw-byte-literal — HTTP 401
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = { create: create };
|