@blamejs/core 0.8.76 → 0.8.77
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 +1 -0
- package/index.js +2 -0
- package/lib/acme.js +200 -1
- package/lib/auth/oauth.js +329 -0
- package/lib/compliance-ai-act.js +161 -3
- package/lib/compliance.js +48 -0
- package/lib/config.js +90 -13
- package/lib/content-credentials.js +227 -0
- package/lib/cra-report.js +106 -2
- package/lib/crypto-field.js +5 -0
- package/lib/dsr.js +96 -0
- package/lib/mcp.js +239 -6
- package/lib/middleware/index.js +4 -0
- package/lib/middleware/protected-resource-metadata.js +165 -0
- package/lib/middleware/rate-limit.js +59 -3
- package/lib/middleware/scim-server.js +375 -0
- package/lib/middleware/security-headers.js +12 -0
- package/lib/nist-crosswalk.js +293 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.middleware.protectedResourceMetadata
|
|
4
|
+
* @nav Identity & access
|
|
5
|
+
* @title Protected Resource Metadata
|
|
6
|
+
* @order 210
|
|
7
|
+
* @slug protected-resource-metadata
|
|
8
|
+
*
|
|
9
|
+
* @intro
|
|
10
|
+
* draft-ietf-oauth-resource-metadata: serves the
|
|
11
|
+
* `/.well-known/oauth-protected-resource` document so RFC 9728
|
|
12
|
+
* clients can auto-discover which authorization servers issue
|
|
13
|
+
* tokens for this resource, what scopes the resource accepts,
|
|
14
|
+
* what dpop algorithms the resource verifies, and which bearer-
|
|
15
|
+
* method binding (DPoP / mTLS cnf claim) is required. Pairs with
|
|
16
|
+
* b.middleware.bearerAuth so a 401 from the protected resource
|
|
17
|
+
* includes `WWW-Authenticate: Bearer resource_metadata=<url>` and
|
|
18
|
+
* the client can self-rediscover.
|
|
19
|
+
*
|
|
20
|
+
* @card
|
|
21
|
+
* `.well-known/oauth-protected-resource` discovery endpoint per
|
|
22
|
+
* draft-ietf-oauth-resource-metadata. Closes the gap that previously
|
|
23
|
+
* forced operators to hand-write the JSON.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
var framework_error = require("../framework-error");
|
|
27
|
+
var validateOpts = require("../validate-opts");
|
|
28
|
+
var requestHelpers = require("../request-helpers");
|
|
29
|
+
|
|
30
|
+
var H = requestHelpers.HTTP_STATUS;
|
|
31
|
+
|
|
32
|
+
var ProtectedResourceMetadataError = framework_error.defineClass(
|
|
33
|
+
"ProtectedResourceMetadataError",
|
|
34
|
+
"middleware/protected-resource-metadata"
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
var ALLOWED_BEARER_METHODS = ["header", "body", "query"];
|
|
38
|
+
var ALLOWED_DPOP_ALGS = ["ES256", "ES384", "RS256", "PS256", "EdDSA", "ML-DSA-65", "ML-DSA-87"];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @primitive b.middleware.protectedResourceMetadata
|
|
42
|
+
* @signature b.middleware.protectedResourceMetadata(opts)
|
|
43
|
+
* @since 0.8.77
|
|
44
|
+
* @related b.auth.oauth.introspectToken, b.middleware.bearerAuth
|
|
45
|
+
*
|
|
46
|
+
* Returns a request middleware that serves the protected-resource
|
|
47
|
+
* metadata JSON document at `/.well-known/oauth-protected-resource`
|
|
48
|
+
* (or operator-overridden path).
|
|
49
|
+
*
|
|
50
|
+
* @opts
|
|
51
|
+
* {
|
|
52
|
+
* resource: string, // canonical resource URI (required)
|
|
53
|
+
* authorizationServers: string[], // issuer URLs that mint tokens for this resource (required, ≥1)
|
|
54
|
+
* scopesSupported?: string[],
|
|
55
|
+
* bearerMethodsSupported?: ("header"|"body"|"query")[], // default ["header"]
|
|
56
|
+
* resourceSigningAlgValuesSupported?: string[], // for signed introspection / jwt-secured responses
|
|
57
|
+
* resourceDocumentation?: string, // URL to operator docs
|
|
58
|
+
* resourcePolicyUri?: string,
|
|
59
|
+
* resourceTosUri?: string,
|
|
60
|
+
* dpopSigningAlgValuesSupported?: string[],
|
|
61
|
+
* dpopBoundAccessTokensRequired?: boolean,
|
|
62
|
+
* mtlsBoundAccessTokensRequired?: boolean,
|
|
63
|
+
* path?: string, // default "/.well-known/oauth-protected-resource"
|
|
64
|
+
* }
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* var mw = b.middleware.protectedResourceMetadata({
|
|
68
|
+
* resource: "https://api.example.com",
|
|
69
|
+
* authorizationServers: ["https://idp.example.com"],
|
|
70
|
+
* scopesSupported: ["read", "write"],
|
|
71
|
+
* dpopBoundAccessTokensRequired: true,
|
|
72
|
+
* });
|
|
73
|
+
* app.use(mw);
|
|
74
|
+
*/
|
|
75
|
+
function create(opts) {
|
|
76
|
+
validateOpts.requireObject(opts, "middleware.protectedResourceMetadata",
|
|
77
|
+
ProtectedResourceMetadataError, "middleware/protected-resource-metadata/bad-opts");
|
|
78
|
+
validateOpts.requireNonEmptyString(opts.resource, "resource",
|
|
79
|
+
ProtectedResourceMetadataError, "middleware/protected-resource-metadata/no-resource");
|
|
80
|
+
|
|
81
|
+
if (!Array.isArray(opts.authorizationServers) || opts.authorizationServers.length === 0) {
|
|
82
|
+
throw new ProtectedResourceMetadataError(
|
|
83
|
+
"middleware/protected-resource-metadata/no-as",
|
|
84
|
+
"authorizationServers must be a non-empty array of issuer URLs");
|
|
85
|
+
}
|
|
86
|
+
opts.authorizationServers.forEach(function (u, i) {
|
|
87
|
+
if (typeof u !== "string" || u.length === 0) {
|
|
88
|
+
throw new ProtectedResourceMetadataError(
|
|
89
|
+
"middleware/protected-resource-metadata/bad-as",
|
|
90
|
+
"authorizationServers[" + i + "] must be a non-empty string");
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
var bearerMethods = opts.bearerMethodsSupported || ["header"];
|
|
95
|
+
bearerMethods.forEach(function (m, i) {
|
|
96
|
+
if (ALLOWED_BEARER_METHODS.indexOf(m) === -1) {
|
|
97
|
+
throw new ProtectedResourceMetadataError(
|
|
98
|
+
"middleware/protected-resource-metadata/bad-bearer-method",
|
|
99
|
+
"bearerMethodsSupported[" + i + "] must be one of: " + ALLOWED_BEARER_METHODS.join(", "));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (opts.dpopSigningAlgValuesSupported) {
|
|
104
|
+
opts.dpopSigningAlgValuesSupported.forEach(function (a, i) {
|
|
105
|
+
if (ALLOWED_DPOP_ALGS.indexOf(a) === -1) {
|
|
106
|
+
throw new ProtectedResourceMetadataError(
|
|
107
|
+
"middleware/protected-resource-metadata/bad-dpop-alg",
|
|
108
|
+
"dpopSigningAlgValuesSupported[" + i + "] = '" + a +
|
|
109
|
+
"' not in allowlist: " + ALLOWED_DPOP_ALGS.join(", "));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
var path = opts.path || "/.well-known/oauth-protected-resource";
|
|
115
|
+
|
|
116
|
+
var doc = {
|
|
117
|
+
resource: opts.resource,
|
|
118
|
+
authorization_servers: opts.authorizationServers,
|
|
119
|
+
bearer_methods_supported: bearerMethods,
|
|
120
|
+
};
|
|
121
|
+
if (opts.scopesSupported) doc.scopes_supported = opts.scopesSupported;
|
|
122
|
+
if (opts.resourceSigningAlgValuesSupported) doc.resource_signing_alg_values_supported = opts.resourceSigningAlgValuesSupported;
|
|
123
|
+
if (opts.resourceDocumentation) doc.resource_documentation = opts.resourceDocumentation;
|
|
124
|
+
if (opts.resourcePolicyUri) doc.resource_policy_uri = opts.resourcePolicyUri;
|
|
125
|
+
if (opts.resourceTosUri) doc.resource_tos_uri = opts.resourceTosUri;
|
|
126
|
+
if (opts.dpopSigningAlgValuesSupported) doc.dpop_signing_alg_values_supported = opts.dpopSigningAlgValuesSupported;
|
|
127
|
+
if (opts.dpopBoundAccessTokensRequired === true) doc.dpop_bound_access_tokens_required = true;
|
|
128
|
+
if (opts.mtlsBoundAccessTokensRequired === true) doc.tls_client_certificate_bound_access_tokens = true;
|
|
129
|
+
|
|
130
|
+
var bodyText = JSON.stringify(doc);
|
|
131
|
+
var bodyBytes = Buffer.byteLength(bodyText, "utf8");
|
|
132
|
+
|
|
133
|
+
function middleware(req, res, next) {
|
|
134
|
+
if (req.url !== path && req.url.split("?")[0] !== path) {
|
|
135
|
+
next();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
139
|
+
res.writeHead(H.METHOD_NOT_ALLOWED, {
|
|
140
|
+
"Allow": "GET, HEAD",
|
|
141
|
+
"Cache-Control": "no-store",
|
|
142
|
+
});
|
|
143
|
+
res.end();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
res.writeHead(H.OK, {
|
|
147
|
+
"Content-Type": "application/json",
|
|
148
|
+
"Content-Length": String(bodyBytes),
|
|
149
|
+
"Cache-Control": "public, max-age=3600",
|
|
150
|
+
});
|
|
151
|
+
if (req.method === "HEAD") { res.end(); return; }
|
|
152
|
+
res.end(bodyText);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
middleware.document = doc;
|
|
156
|
+
middleware.path = path;
|
|
157
|
+
return middleware;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
create: create,
|
|
162
|
+
ProtectedResourceMetadataError: ProtectedResourceMetadataError,
|
|
163
|
+
ALLOWED_BEARER_METHODS: ALLOWED_BEARER_METHODS,
|
|
164
|
+
ALLOWED_DPOP_ALGS: ALLOWED_DPOP_ALGS,
|
|
165
|
+
};
|
|
@@ -159,12 +159,16 @@ function _memoryTokenBucketBackend(opts) {
|
|
|
159
159
|
buckets.delete(key);
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
function resetAll() {
|
|
163
|
+
buckets.clear();
|
|
164
|
+
}
|
|
165
|
+
|
|
162
166
|
function close() {
|
|
163
167
|
try { gcInterval.stop(); } catch (_e) { /* timer already stopped */ }
|
|
164
168
|
buckets.clear();
|
|
165
169
|
}
|
|
166
170
|
|
|
167
|
-
return { take: take, reset: reset, close: close };
|
|
171
|
+
return { take: take, reset: reset, resetAll: resetAll, close: close };
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
// Fixed-window in-memory algorithm — per-key counter that resets at the
|
|
@@ -224,12 +228,14 @@ function _memoryFixedWindowBackend(opts) {
|
|
|
224
228
|
|
|
225
229
|
function reset(key) { counters.delete(key); }
|
|
226
230
|
|
|
231
|
+
function resetAll() { counters.clear(); }
|
|
232
|
+
|
|
227
233
|
function close() {
|
|
228
234
|
try { gcInterval.stop(); } catch (_e) { /* timer already stopped */ }
|
|
229
235
|
counters.clear();
|
|
230
236
|
}
|
|
231
237
|
|
|
232
|
-
return { take: take, reset: reset, close: close };
|
|
238
|
+
return { take: take, reset: reset, resetAll: resetAll, close: close };
|
|
233
239
|
}
|
|
234
240
|
|
|
235
241
|
// ---- Cluster backend (fixed-window counter, SQL-backed) ----
|
|
@@ -485,13 +491,63 @@ function create(opts) {
|
|
|
485
491
|
|
|
486
492
|
// Expose a couple of operator hooks on the middleware function.
|
|
487
493
|
middleware.reset = function (key) { return backend.reset(key); };
|
|
488
|
-
|
|
494
|
+
// Global drop-all for the in-memory backend. Used by incident-
|
|
495
|
+
// response workflows ("operator confirmed false-positive lockout
|
|
496
|
+
// wave, drop the whole table") + by test suites that need a clean
|
|
497
|
+
// slate between cases without re-creating the middleware. For the
|
|
498
|
+
// cluster backend this is a no-op (cluster backends are
|
|
499
|
+
// multi-process and require operator-side coordination — flushing
|
|
500
|
+
// a shared row table from one replica races every other replica's
|
|
501
|
+
// in-flight take() calls).
|
|
502
|
+
middleware.resetAll = function () {
|
|
503
|
+
if (typeof backend.resetAll === "function") return backend.resetAll();
|
|
504
|
+
return null;
|
|
505
|
+
};
|
|
506
|
+
middleware.close = function () {
|
|
507
|
+
_instances.delete(middleware);
|
|
508
|
+
return backend.close && backend.close();
|
|
509
|
+
};
|
|
489
510
|
|
|
511
|
+
_instances.add(middleware);
|
|
490
512
|
return middleware;
|
|
491
513
|
}
|
|
492
514
|
|
|
515
|
+
// Module-level registry of every rate-limit middleware in the running
|
|
516
|
+
// process. Operators reach for this during incident response: when a
|
|
517
|
+
// false-positive lockout wave hits, an oncall script can iterate
|
|
518
|
+
// `instances()` and call `.resetAll()` on each, without having to
|
|
519
|
+
// thread a reference to every rate-limit middleware through wherever
|
|
520
|
+
// the response code runs. Tests use it to assert a clean slate.
|
|
521
|
+
//
|
|
522
|
+
// Lifetime: a middleware joins on `create()` return and leaves on
|
|
523
|
+
// `middleware.close()`. Long-lived servers create rate-limiters once at
|
|
524
|
+
// boot; throwaway middlewares (tests, sandboxes) must close() to
|
|
525
|
+
// deregister. We deliberately don't use WeakRef here — operators want
|
|
526
|
+
// strong, observable membership ("did this rate-limiter actually get
|
|
527
|
+
// created?"), and the count is bounded by how many limiters an app
|
|
528
|
+
// configures, not how much traffic it sees.
|
|
529
|
+
var _instances = new Set();
|
|
530
|
+
|
|
531
|
+
function instances() {
|
|
532
|
+
return Array.from(_instances);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Global drop-all across every middleware in the process. Returns the
|
|
536
|
+
// number of instances that responded to resetAll (cluster-backed
|
|
537
|
+
// middlewares no-op their own resetAll but still count toward the
|
|
538
|
+
// total so operators see all instances were addressed).
|
|
539
|
+
function resetAll() {
|
|
540
|
+
var n = 0;
|
|
541
|
+
_instances.forEach(function (m) {
|
|
542
|
+
try { m.resetAll(); n += 1; } catch (_e) { /* best-effort */ }
|
|
543
|
+
});
|
|
544
|
+
return n;
|
|
545
|
+
}
|
|
546
|
+
|
|
493
547
|
module.exports = {
|
|
494
548
|
create: create,
|
|
549
|
+
instances: instances,
|
|
550
|
+
resetAll: resetAll,
|
|
495
551
|
// Backends exported for tests + advanced operator wiring.
|
|
496
552
|
_memoryBackend: _memoryBackend,
|
|
497
553
|
_memoryTokenBucketBackend: _memoryTokenBucketBackend,
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SCIM 2.0 server middleware (RFC 7642 / 7643 / 7644).
|
|
3
|
+
// Provides /Users + /Groups + /ServiceProviderConfig + /ResourceTypes
|
|
4
|
+
// + /Schemas surfaces backed by operator-supplied CRUD callbacks.
|
|
5
|
+
|
|
6
|
+
var framework_error = require("../framework-error");
|
|
7
|
+
var validateOpts = require("../validate-opts");
|
|
8
|
+
var safeJson = require("../safe-json");
|
|
9
|
+
var safeBuffer = require("../safe-buffer");
|
|
10
|
+
var requestHelpers = require("../request-helpers");
|
|
11
|
+
var C = require("../constants");
|
|
12
|
+
|
|
13
|
+
var H = requestHelpers.HTTP_STATUS;
|
|
14
|
+
|
|
15
|
+
var ScimServerError = framework_error.defineClass(
|
|
16
|
+
"ScimServerError",
|
|
17
|
+
"middleware/scim-server"
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
var SCIM_CORE_SCHEMA_USER = "urn:ietf:params:scim:schemas:core:2.0:User";
|
|
21
|
+
var SCIM_CORE_SCHEMA_GROUP = "urn:ietf:params:scim:schemas:core:2.0:Group";
|
|
22
|
+
var SCIM_MESSAGE_ERROR = "urn:ietf:params:scim:api:messages:2.0:Error";
|
|
23
|
+
var SCIM_MESSAGE_LIST = "urn:ietf:params:scim:api:messages:2.0:ListResponse";
|
|
24
|
+
|
|
25
|
+
var ALLOWED_FILTER_OPS = ["eq", "ne", "co", "sw", "ew", "pr", "gt", "ge", "lt", "le"];
|
|
26
|
+
|
|
27
|
+
var SCIM_FILTER_RE = /^\s*([a-zA-Z][a-zA-Z0-9._-]*)\s+(eq|ne|co|sw|ew|pr|gt|ge|lt|le)(?:\s+(.+))?\s*$/;
|
|
28
|
+
var RESOURCE_PATH_RE = /^\/(Users|Groups)(?:\/([^/]+))?$/;
|
|
29
|
+
var BEARER_RE = /^Bearer\s+(.+)$/i;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @primitive b.middleware.scimServer
|
|
33
|
+
* @signature b.middleware.scimServer(opts)
|
|
34
|
+
* @since 0.8.77
|
|
35
|
+
* @related b.auth.oauth.introspectToken
|
|
36
|
+
*
|
|
37
|
+
* Returns a request middleware that handles SCIM 2.0 requests
|
|
38
|
+
* (RFC 7642-7644). Operator supplies CRUD callbacks per resource.
|
|
39
|
+
*
|
|
40
|
+
* @opts
|
|
41
|
+
* {
|
|
42
|
+
* basePath?: string,
|
|
43
|
+
* users: ScimResourceImpl,
|
|
44
|
+
* groups?: ScimResourceImpl,
|
|
45
|
+
* bearer?: (token) => Promise<actor>,
|
|
46
|
+
* maxPageSize?: number,
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* var mw = b.middleware.scimServer({
|
|
51
|
+
* basePath: "/scim/v2",
|
|
52
|
+
* users: myUserAdapter,
|
|
53
|
+
* groups: myGroupAdapter,
|
|
54
|
+
* });
|
|
55
|
+
* app.use(mw);
|
|
56
|
+
*/
|
|
57
|
+
function create(opts) {
|
|
58
|
+
validateOpts.requireObject(opts, "middleware.scimServer",
|
|
59
|
+
ScimServerError, "middleware/scim-server/bad-opts");
|
|
60
|
+
_validateResourceImpl(opts.users, "users");
|
|
61
|
+
if (opts.groups) _validateResourceImpl(opts.groups, "groups");
|
|
62
|
+
|
|
63
|
+
var basePath = opts.basePath || "/scim/v2";
|
|
64
|
+
var maxPageSize = opts.maxPageSize || 200; // allow:raw-byte-literal — page-size count, not bytes
|
|
65
|
+
var bearer = opts.bearer || null;
|
|
66
|
+
|
|
67
|
+
function middleware(req, res, next) {
|
|
68
|
+
var url = req.url.split("?")[0];
|
|
69
|
+
if (url.indexOf(basePath) !== 0) { next(); return; }
|
|
70
|
+
_dispatch(req, res, basePath, bearer, opts, maxPageSize)
|
|
71
|
+
.catch(function (err) {
|
|
72
|
+
_writeScimError(res, err.statusCode || 500, err.scimType || "internal",
|
|
73
|
+
(err.message || String(err)).slice(0, 500));
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
middleware.basePath = basePath;
|
|
78
|
+
middleware.serviceProviderDoc = _serviceProviderConfig(opts);
|
|
79
|
+
return middleware;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function _validateResourceImpl(impl, name) {
|
|
83
|
+
if (!impl || typeof impl !== "object") {
|
|
84
|
+
throw new ScimServerError("middleware/scim-server/no-" + name,
|
|
85
|
+
"middleware.scimServer: opts." + name + " must be an object implementing { create, read, update, patch, remove, list }");
|
|
86
|
+
}
|
|
87
|
+
["create", "read", "update", "patch", "remove", "list"].forEach(function (m) {
|
|
88
|
+
if (typeof impl[m] !== "function") {
|
|
89
|
+
throw new ScimServerError("middleware/scim-server/bad-" + name + "-impl",
|
|
90
|
+
"middleware.scimServer: opts." + name + "." + m + " must be a function");
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function _dispatch(req, res, basePath, bearer, opts, maxPageSize) {
|
|
96
|
+
var relUrl = req.url.slice(basePath.length) || "/";
|
|
97
|
+
var qIdx = relUrl.indexOf("?");
|
|
98
|
+
var path = qIdx === -1 ? relUrl : relUrl.slice(0, qIdx);
|
|
99
|
+
var query = _parseQuery(qIdx === -1 ? "" : relUrl.slice(qIdx + 1));
|
|
100
|
+
|
|
101
|
+
if (path === "/ServiceProviderConfig" && req.method === "GET") {
|
|
102
|
+
_writeJson(res, H.OK, _serviceProviderConfig(opts)); return;
|
|
103
|
+
}
|
|
104
|
+
if (path === "/ResourceTypes" && req.method === "GET") {
|
|
105
|
+
_writeJson(res, H.OK, _resourceTypes(opts)); return;
|
|
106
|
+
}
|
|
107
|
+
if (path === "/Schemas" && req.method === "GET") {
|
|
108
|
+
_writeJson(res, H.OK, _schemas()); return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
var actor = null;
|
|
112
|
+
if (bearer) {
|
|
113
|
+
var authz = req.headers && req.headers.authorization;
|
|
114
|
+
// BEARER_RE applies to a single header line; node http caps header lines at 8 KiB.
|
|
115
|
+
var m = authz && typeof authz === "string" && authz.length < C.BYTES.kib(8) && BEARER_RE.test(authz) // allow:regex-no-length-cap header line bounded above
|
|
116
|
+
? authz.match(BEARER_RE) : null;
|
|
117
|
+
if (!m) {
|
|
118
|
+
res.writeHead(H.UNAUTHORIZED, {
|
|
119
|
+
"Content-Type": "application/scim+json",
|
|
120
|
+
"WWW-Authenticate": "Bearer",
|
|
121
|
+
"Cache-Control": "no-store",
|
|
122
|
+
});
|
|
123
|
+
res.end(JSON.stringify({
|
|
124
|
+
schemas: [SCIM_MESSAGE_ERROR],
|
|
125
|
+
status: "401",
|
|
126
|
+
detail: "Missing Bearer token",
|
|
127
|
+
}));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
try { actor = await bearer(m[1]); }
|
|
131
|
+
catch (_e) { actor = null; }
|
|
132
|
+
if (!actor) {
|
|
133
|
+
res.writeHead(H.UNAUTHORIZED, {
|
|
134
|
+
"Content-Type": "application/scim+json",
|
|
135
|
+
"WWW-Authenticate": 'Bearer error="invalid_token"',
|
|
136
|
+
"Cache-Control": "no-store",
|
|
137
|
+
});
|
|
138
|
+
res.end(JSON.stringify({
|
|
139
|
+
schemas: [SCIM_MESSAGE_ERROR],
|
|
140
|
+
status: "401",
|
|
141
|
+
detail: "Bearer token rejected",
|
|
142
|
+
}));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
var ctx = { actor: actor, req: req };
|
|
148
|
+
var users = opts.users;
|
|
149
|
+
var groups = opts.groups;
|
|
150
|
+
|
|
151
|
+
// RESOURCE_PATH_RE applies to a URL path; node http caps URL at 8 KiB.
|
|
152
|
+
var match = path.length < C.BYTES.kib(8) && RESOURCE_PATH_RE.test(path) ? path.match(RESOURCE_PATH_RE) : null; // allow:regex-no-length-cap URL bounded above
|
|
153
|
+
if (!match) {
|
|
154
|
+
_writeScimError(res, H.NOT_FOUND, "notFound", "no SCIM resource at " + path);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
var resourceType = match[1];
|
|
158
|
+
var resourceId = match[2] || null;
|
|
159
|
+
var impl = resourceType === "Users" ? users : groups;
|
|
160
|
+
if (!impl) {
|
|
161
|
+
_writeScimError(res, 404, "notFound", "/" + resourceType + " not configured");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
var body = null;
|
|
166
|
+
if (req.method === "POST" || req.method === "PUT" || req.method === "PATCH") {
|
|
167
|
+
body = await _readJsonBody(req);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (req.method === "GET" && !resourceId) {
|
|
171
|
+
var filter = _parseFilter(query.filter);
|
|
172
|
+
var pageSize = Math.min(maxPageSize, parseInt(query.count || "100", 10) || 100);
|
|
173
|
+
var startIndex = Math.max(1, parseInt(query.startIndex || "1", 10) || 1);
|
|
174
|
+
var listRv = await impl.list({
|
|
175
|
+
filter: filter,
|
|
176
|
+
startIndex: startIndex,
|
|
177
|
+
count: pageSize,
|
|
178
|
+
sortBy: query.sortBy || null,
|
|
179
|
+
sortOrder: query.sortOrder || null,
|
|
180
|
+
attributes: query.attributes ? query.attributes.split(",") : null,
|
|
181
|
+
excludedAttributes: query.excludedAttributes ? query.excludedAttributes.split(",") : null,
|
|
182
|
+
}, ctx);
|
|
183
|
+
_writeJson(res, H.OK, {
|
|
184
|
+
schemas: [SCIM_MESSAGE_LIST],
|
|
185
|
+
totalResults: listRv.totalResults,
|
|
186
|
+
startIndex: startIndex,
|
|
187
|
+
itemsPerPage: listRv.Resources.length,
|
|
188
|
+
Resources: listRv.Resources,
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (req.method === "GET" && resourceId) {
|
|
194
|
+
var rec = await impl.read(resourceId, ctx);
|
|
195
|
+
if (!rec) { _writeScimError(res, H.NOT_FOUND, "notFound", "no resource with id " + resourceId); return; }
|
|
196
|
+
_writeJson(res, H.OK, rec);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (req.method === "POST" && !resourceId) {
|
|
201
|
+
_assertSchema(body, resourceType === "Users" ? SCIM_CORE_SCHEMA_USER : SCIM_CORE_SCHEMA_GROUP);
|
|
202
|
+
var created = await impl.create(body, ctx);
|
|
203
|
+
_writeJson(res, H.CREATED, created);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (req.method === "PUT" && resourceId) {
|
|
208
|
+
_assertSchema(body, resourceType === "Users" ? SCIM_CORE_SCHEMA_USER : SCIM_CORE_SCHEMA_GROUP);
|
|
209
|
+
var updated = await impl.update(resourceId, body, ctx);
|
|
210
|
+
_writeJson(res, H.OK, updated);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (req.method === "PATCH" && resourceId) {
|
|
215
|
+
if (!body || !Array.isArray(body.Operations) || body.Operations.length === 0) {
|
|
216
|
+
_writeScimError(res, H.BAD_REQUEST, "invalidValue", "PATCH body must include Operations[]");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
body.Operations.forEach(function (op, i) {
|
|
220
|
+
if (!op || typeof op !== "object" || typeof op.op !== "string") {
|
|
221
|
+
var e = new Error("Operations[" + i + "] missing op");
|
|
222
|
+
e.statusCode = H.BAD_REQUEST; e.scimType = "invalidValue";
|
|
223
|
+
throw e;
|
|
224
|
+
}
|
|
225
|
+
var verb = op.op.toLowerCase();
|
|
226
|
+
if (verb !== "add" && verb !== "remove" && verb !== "replace") {
|
|
227
|
+
var e2 = new Error("Operations[" + i + "].op = '" + op.op + "' not in add/remove/replace");
|
|
228
|
+
e2.statusCode = H.BAD_REQUEST; e2.scimType = "invalidValue";
|
|
229
|
+
throw e2;
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
var patched = await impl.patch(resourceId, body.Operations, ctx);
|
|
233
|
+
_writeJson(res, H.OK, patched);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (req.method === "DELETE" && resourceId) {
|
|
238
|
+
await impl.remove(resourceId, ctx);
|
|
239
|
+
res.writeHead(H.NO_CONTENT, { "Cache-Control": "no-store" });
|
|
240
|
+
res.end();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
_writeScimError(res, H.METHOD_NOT_ALLOWED, "noTarget", req.method + " not allowed on " + path);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function _parseQuery(qs) {
|
|
248
|
+
var out = {};
|
|
249
|
+
if (!qs) return out;
|
|
250
|
+
qs.split("&").forEach(function (pair) {
|
|
251
|
+
var eq = pair.indexOf("=");
|
|
252
|
+
var k = eq === -1 ? pair : pair.slice(0, eq);
|
|
253
|
+
var v = eq === -1 ? "" : pair.slice(eq + 1);
|
|
254
|
+
try { out[decodeURIComponent(k)] = decodeURIComponent(v.replace(/\+/g, " ")); }
|
|
255
|
+
catch (_e) { /* skip malformed */ }
|
|
256
|
+
});
|
|
257
|
+
return out;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function _parseFilter(filter) {
|
|
261
|
+
if (typeof filter !== "string" || filter.length === 0) return null;
|
|
262
|
+
if (!SCIM_FILTER_RE.test(filter)) return { raw: filter };
|
|
263
|
+
var m = filter.match(SCIM_FILTER_RE);
|
|
264
|
+
var op = m[2].toLowerCase();
|
|
265
|
+
if (ALLOWED_FILTER_OPS.indexOf(op) === -1) return { raw: filter };
|
|
266
|
+
var rv = { attribute: m[1], op: op, raw: filter };
|
|
267
|
+
if (op === "pr") return rv;
|
|
268
|
+
var v = (m[3] || "").trim();
|
|
269
|
+
if (v.charAt(0) === '"' && v.charAt(v.length - 1) === '"') {
|
|
270
|
+
v = v.slice(1, -1).replace(/\\"/g, '"');
|
|
271
|
+
}
|
|
272
|
+
rv.value = v;
|
|
273
|
+
return rv;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function _readJsonBody(req) {
|
|
277
|
+
var MAX = C.BYTES.mib(1);
|
|
278
|
+
if (req.body && Buffer.isBuffer(req.body)) {
|
|
279
|
+
return Promise.resolve(safeJson.parse(req.body.toString("utf8"), { maxBytes: MAX }));
|
|
280
|
+
}
|
|
281
|
+
if (req.body && typeof req.body === "object") return Promise.resolve(req.body);
|
|
282
|
+
return safeBuffer.boundedChunkCollector(req, MAX, ScimServerError, "middleware/scim-server/body-too-large")
|
|
283
|
+
.then(function (buf) {
|
|
284
|
+
return safeJson.parse(buf.toString("utf8"), { maxBytes: MAX });
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function _assertSchema(body, expectedSchema) {
|
|
289
|
+
if (!body || typeof body !== "object") {
|
|
290
|
+
var e = new Error("request body must be a JSON object");
|
|
291
|
+
e.statusCode = H.BAD_REQUEST; e.scimType = "invalidValue"; throw e;
|
|
292
|
+
}
|
|
293
|
+
if (!Array.isArray(body.schemas) || body.schemas.indexOf(expectedSchema) === -1) {
|
|
294
|
+
var e2 = new Error("body.schemas must include '" + expectedSchema + "'");
|
|
295
|
+
e2.statusCode = H.BAD_REQUEST; e2.scimType = "invalidValue"; throw e2;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function _writeJson(res, status, body) {
|
|
300
|
+
res.writeHead(status, {
|
|
301
|
+
"Content-Type": "application/scim+json",
|
|
302
|
+
"Cache-Control": "no-store",
|
|
303
|
+
});
|
|
304
|
+
res.end(JSON.stringify(body));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function _writeScimError(res, status, scimType, detail) {
|
|
308
|
+
res.writeHead(status, {
|
|
309
|
+
"Content-Type": "application/scim+json",
|
|
310
|
+
"Cache-Control": "no-store",
|
|
311
|
+
});
|
|
312
|
+
res.end(JSON.stringify({
|
|
313
|
+
schemas: [SCIM_MESSAGE_ERROR],
|
|
314
|
+
status: String(status),
|
|
315
|
+
scimType: scimType,
|
|
316
|
+
detail: detail,
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function _serviceProviderConfig(opts) {
|
|
321
|
+
return {
|
|
322
|
+
schemas: ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
|
|
323
|
+
documentationUri: opts.documentationUri || "https://datatracker.ietf.org/doc/html/rfc7643",
|
|
324
|
+
patch: { supported: true },
|
|
325
|
+
bulk: { supported: false, maxOperations: 0, maxPayloadSize: 0 },
|
|
326
|
+
filter: { supported: true, maxResults: opts.maxPageSize || 200 },
|
|
327
|
+
changePassword: { supported: false },
|
|
328
|
+
sort: { supported: true },
|
|
329
|
+
etag: { supported: false },
|
|
330
|
+
authenticationSchemes: [
|
|
331
|
+
{
|
|
332
|
+
type: "oauthbearertoken",
|
|
333
|
+
name: "OAuth 2.0 Bearer Token",
|
|
334
|
+
description: "Authentication scheme using the OAuth Bearer Token Standard",
|
|
335
|
+
specUri: "https://www.rfc-editor.org/info/rfc6750",
|
|
336
|
+
primary: true,
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function _resourceTypes(opts) {
|
|
343
|
+
var rv = [
|
|
344
|
+
{
|
|
345
|
+
schemas: ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
|
|
346
|
+
id: "User", name: "User", endpoint: "/Users",
|
|
347
|
+
description: "User resource (RFC 7643 §4.1)",
|
|
348
|
+
schema: SCIM_CORE_SCHEMA_USER,
|
|
349
|
+
},
|
|
350
|
+
];
|
|
351
|
+
if (opts.groups) {
|
|
352
|
+
rv.push({
|
|
353
|
+
schemas: ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
|
|
354
|
+
id: "Group", name: "Group", endpoint: "/Groups",
|
|
355
|
+
description: "Group resource (RFC 7643 §4.2)",
|
|
356
|
+
schema: SCIM_CORE_SCHEMA_GROUP,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return rv;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function _schemas() {
|
|
363
|
+
return [
|
|
364
|
+
{ id: SCIM_CORE_SCHEMA_USER, name: "User", description: "RFC 7643 §4.1 User resource" },
|
|
365
|
+
{ id: SCIM_CORE_SCHEMA_GROUP, name: "Group", description: "RFC 7643 §4.2 Group resource" },
|
|
366
|
+
];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
module.exports = {
|
|
370
|
+
create: create,
|
|
371
|
+
ScimServerError: ScimServerError,
|
|
372
|
+
SCIM_CORE_SCHEMA_USER: SCIM_CORE_SCHEMA_USER,
|
|
373
|
+
SCIM_CORE_SCHEMA_GROUP: SCIM_CORE_SCHEMA_GROUP,
|
|
374
|
+
ALLOWED_FILTER_OPS: ALLOWED_FILTER_OPS,
|
|
375
|
+
};
|
|
@@ -71,6 +71,18 @@ var DEFAULT_PERMISSIONS = [
|
|
|
71
71
|
// embedding APIs; deny-by-default.
|
|
72
72
|
"storage-access=()", "browsing-topics=()",
|
|
73
73
|
"private-aggregation=()", "controlled-frame=()", "captured-surface-control=()",
|
|
74
|
+
// v0.8.77 expansion — remaining Privacy Sandbox + Browser-API
|
|
75
|
+
// directives surfacing through Chrome 130+ / Firefox 132+ stable.
|
|
76
|
+
// Default-deny: FedCM (identity-credentials-get), cross-site
|
|
77
|
+
// attribution reporting, WebAuthn create flow (operators that need
|
|
78
|
+
// it opt in explicitly), FLEDGE/Topics auction APIs (join-ad-
|
|
79
|
+
// interest-group / run-ad-auction), Shared Storage API + selectURL,
|
|
80
|
+
// Smart Card API, all-screens capture, deferred-fetch (background
|
|
81
|
+
// resource sync).
|
|
82
|
+
"identity-credentials-get=()", "attribution-reporting-cross-site=()",
|
|
83
|
+
"publickey-credentials-create=()", "join-ad-interest-group=()",
|
|
84
|
+
"run-ad-auction=()", "shared-storage=()", "shared-storage-select-url=()",
|
|
85
|
+
"smartcard=()", "all-screens-capture=()", "deferred-fetch=()",
|
|
74
86
|
];
|
|
75
87
|
|
|
76
88
|
// Strict CSP — no 'unsafe-inline' on script-src OR style-src.
|