@blamejs/core 0.7.18 → 0.7.19
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 +425 -423
- package/README.md +150 -150
- package/bin/blamejs.js +0 -0
- package/index.js +310 -308
- package/lib/api-key.js +660 -660
- package/lib/api-snapshot.js +338 -338
- package/lib/app-shutdown.js +385 -385
- package/lib/app.js +365 -365
- package/lib/archive.js +250 -250
- package/lib/atomic-file.js +544 -544
- package/lib/audit-chain.js +177 -177
- package/lib/audit-sign.js +344 -344
- package/lib/audit-tools.js +677 -677
- package/lib/audit.js +766 -766
- package/lib/auth/jwt-external.js +365 -0
- package/lib/auth/jwt.js +337 -311
- package/lib/auth/lockout.js +436 -436
- package/lib/auth/oauth.js +721 -721
- package/lib/auth/passkey.js +181 -181
- package/lib/auth/password.js +628 -594
- package/lib/backup/bundle.js +217 -217
- package/lib/backup/crypto.js +176 -176
- package/lib/backup/index.js +515 -515
- package/lib/backup/manifest.js +282 -282
- package/lib/break-glass.js +1338 -1338
- package/lib/bundler.js +441 -441
- package/lib/cache-redis.js +256 -256
- package/lib/cache.js +1206 -1206
- package/lib/canonical-json.js +115 -115
- package/lib/chain-writer.js +234 -234
- package/lib/cli-helpers.js +206 -206
- package/lib/cli.js +2334 -2334
- package/lib/cluster-provider-db.js +317 -317
- package/lib/cluster-storage.js +226 -226
- package/lib/cluster.js +703 -703
- package/lib/config-drift.js +301 -301
- package/lib/consent.js +222 -222
- package/lib/constants.js +191 -191
- package/lib/cookies.js +315 -315
- package/lib/credential-hash.js +322 -322
- package/lib/crypto.js +266 -266
- package/lib/csv.js +275 -275
- package/lib/db-declare-row-policy.js +267 -267
- package/lib/db-declare-view.js +420 -420
- package/lib/db-query.js +406 -406
- package/lib/db-schema.js +319 -319
- package/lib/db.js +1288 -1288
- package/lib/deprecate.js +222 -222
- package/lib/dev.js +335 -335
- package/lib/dual-control.js +473 -473
- package/lib/error-page.js +420 -420
- package/lib/external-db-migrate.js +441 -441
- package/lib/external-db.js +1061 -1061
- package/lib/file-type.js +273 -273
- package/lib/forms.js +422 -422
- package/lib/framework-error.js +293 -293
- package/lib/framework-schema.js +717 -717
- package/lib/handlers.js +350 -350
- package/lib/http-client-cookie-jar.js +508 -508
- package/lib/http-client.js +1195 -1195
- package/lib/i18n.js +878 -878
- package/lib/jobs.js +185 -185
- package/lib/log-stream-cloudwatch.js +369 -369
- package/lib/log-stream-local.js +146 -146
- package/lib/log-stream-otlp-grpc.js +410 -410
- package/lib/log-stream-otlp.js +286 -286
- package/lib/log-stream-syslog.js +302 -302
- package/lib/log-stream-webhook.js +199 -199
- package/lib/log-stream.js +330 -330
- package/lib/log.js +500 -500
- package/lib/mail-bounce.js +528 -528
- package/lib/mail-dkim.js +369 -369
- package/lib/mail.js +981 -981
- package/lib/metrics.js +683 -683
- package/lib/middleware/api-encrypt.js +936 -936
- package/lib/middleware/attach-user.js +157 -157
- package/lib/middleware/bearer-auth.js +152 -0
- package/lib/middleware/body-parser.js +1170 -1170
- package/lib/middleware/bot-guard.js +178 -178
- package/lib/middleware/compression.js +452 -452
- package/lib/middleware/cors.js +314 -314
- package/lib/middleware/csp-nonce.js +348 -348
- package/lib/middleware/csrf-protect.js +316 -316
- package/lib/middleware/db-role-for.js +264 -264
- package/lib/middleware/health.js +392 -392
- package/lib/middleware/index.js +82 -79
- package/lib/middleware/rate-limit.js +358 -358
- package/lib/middleware/request-id.js +61 -61
- package/lib/middleware/request-log.js +168 -168
- package/lib/middleware/require-auth.js +104 -104
- package/lib/middleware/security-headers.js +116 -116
- package/lib/middleware/sse.js +166 -166
- package/lib/migrations.js +383 -383
- package/lib/mtls-ca.js +518 -518
- package/lib/mtls-engine-default.js +481 -481
- package/lib/network-dns.js +632 -632
- package/lib/network-heartbeat.js +290 -290
- package/lib/network-nts.js +574 -574
- package/lib/network-proxy.js +265 -265
- package/lib/network-tls.js +328 -328
- package/lib/network.js +233 -233
- package/lib/notify.js +612 -612
- package/lib/ntp-check.js +229 -229
- package/lib/numeric-bounds.js +111 -111
- package/lib/object-store/azure-blob-bucket-ops.js +349 -349
- package/lib/object-store/azure-blob.js +488 -488
- package/lib/object-store/gcs-bucket-ops.js +351 -351
- package/lib/object-store/gcs.js +519 -519
- package/lib/object-store/http-put.js +153 -153
- package/lib/object-store/index.js +197 -197
- package/lib/object-store/sigv4-bucket-ops.js +1092 -1092
- package/lib/object-store/sigv4.js +903 -903
- package/lib/observability.js +151 -151
- package/lib/otel-export.js +269 -269
- package/lib/pagination.js +464 -464
- package/lib/parsers/index.js +80 -80
- package/lib/parsers/safe-env.js +642 -642
- package/lib/parsers/safe-ini.js +292 -292
- package/lib/parsers/safe-toml.js +784 -784
- package/lib/parsers/safe-xml.js +390 -390
- package/lib/parsers/safe-yaml.js +1015 -1015
- package/lib/permissions.js +708 -708
- package/lib/pqc-agent.js +87 -87
- package/lib/pqc-gate.js +279 -279
- package/lib/protobuf-encoder.js +190 -190
- package/lib/protocol-dispatcher.js +161 -161
- package/lib/pubsub-redis.js +167 -167
- package/lib/pubsub.js +429 -429
- package/lib/queue-local.js +476 -476
- package/lib/queue-redis.js +745 -745
- package/lib/queue-sqs.js +319 -319
- package/lib/queue.js +695 -695
- package/lib/redis-client.js +519 -519
- package/lib/request-helpers.js +340 -340
- package/lib/restore-bundle.js +237 -237
- package/lib/restore-rollback.js +259 -259
- package/lib/restore.js +409 -409
- package/lib/retry.js +376 -376
- package/lib/router.js +748 -748
- package/lib/safe-async.js +735 -735
- package/lib/safe-buffer.js +237 -237
- package/lib/safe-json.js +541 -541
- package/lib/safe-schema.js +1266 -1266
- package/lib/safe-url.js +159 -159
- package/lib/scheduler.js +706 -706
- package/lib/security-assert.js +373 -373
- package/lib/seeders.js +618 -618
- package/lib/session.js +535 -478
- package/lib/slug.js +269 -269
- package/lib/ssrf-guard.js +401 -401
- package/lib/storage.js +471 -471
- package/lib/subject.js +281 -281
- package/lib/template.js +791 -791
- package/lib/testing.js +798 -798
- package/lib/time.js +310 -310
- package/lib/totp.js +302 -302
- package/lib/tracing.js +494 -494
- package/lib/uuid.js +132 -132
- package/lib/validate-opts.js +340 -340
- package/lib/vault/index.js +308 -308
- package/lib/vault/rotate.js +784 -784
- package/lib/vault/wrap.js +296 -296
- package/lib/vendor/noble-ciphers.cjs +9 -9
- package/lib/webhook.js +595 -595
- package/lib/websocket.js +1048 -1048
- package/package.json +77 -77
- package/sbom.cyclonedx.json +7 -7
|
@@ -1,157 +1,157 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* attach-user middleware — populates req.user (and req.session) from a
|
|
4
|
-
* verified session token.
|
|
5
|
-
*
|
|
6
|
-
* Token sources, tried in order:
|
|
7
|
-
* 1. Cookie named opts.cookieName (default "blamejs_session")
|
|
8
|
-
* 2. Authorization: Bearer <token> header
|
|
9
|
-
* The first one that produces a verified session wins. Operators who
|
|
10
|
-
* want only one path can disable the other via opts.tokenFrom.
|
|
11
|
-
*
|
|
12
|
-
* Sealed cookies (access-gated): when opts.sealed is true and opts.vault
|
|
13
|
-
* is provided, the cookie value is read via cookies.readSealed (which
|
|
14
|
-
* vault-unseals and recovers the original session token). Without the
|
|
15
|
-
* framework's vault key no client can hand-craft a valid cookie, so the
|
|
16
|
-
* API is unreachable via curl-with-arbitrary-cookies. Default off for
|
|
17
|
-
* back-compat with apps that put the raw token in the cookie.
|
|
18
|
-
*
|
|
19
|
-
* The framework can't know the application's user schema, so the
|
|
20
|
-
* middleware delegates user-record loading to opts.userLoader — an
|
|
21
|
-
* async function `(verifiedSession) => user | null`. Operators
|
|
22
|
-
* typically look up by userId in their own users table.
|
|
23
|
-
*
|
|
24
|
-
* Failure modes (none of which throw):
|
|
25
|
-
* - No token in either source → req.user = null, next()
|
|
26
|
-
* - Sealed cookie present but unsealable → req.user = null, next()
|
|
27
|
-
* - Token present but session.verify rejects → req.user = null, next()
|
|
28
|
-
* - Session valid but userLoader returns null/undefined → req.user
|
|
29
|
-
* = null, next() + audit emit (with outcome=failure)
|
|
30
|
-
*
|
|
31
|
-
* Always calls next() — the gating decision is downstream's job (use
|
|
32
|
-
* middleware.requireAuth for that). This middleware only ATTACHES.
|
|
33
|
-
*
|
|
34
|
-
* Options:
|
|
35
|
-
* {
|
|
36
|
-
* cookieName: 'blamejs_session' (cookie name to read)
|
|
37
|
-
* tokenFrom: 'both' | 'cookie' | 'header' (default 'both')
|
|
38
|
-
* sealed: false (use cookies.readSealed)
|
|
39
|
-
* vault: b.vault (required when sealed)
|
|
40
|
-
* userLoader: async (verifiedSession) => user (REQUIRED)
|
|
41
|
-
* audit: true (emit user-load audits)
|
|
42
|
-
* }
|
|
43
|
-
*/
|
|
44
|
-
var lazyRequire = require("../lazy-require");
|
|
45
|
-
var cookies = require("../cookies");
|
|
46
|
-
var requestHelpers = require("../request-helpers");
|
|
47
|
-
var validateOpts = require("../validate-opts");
|
|
48
|
-
var session = lazyRequire(function () { return require("../session"); });
|
|
49
|
-
var audit = lazyRequire(function () { return require("../audit"); });
|
|
50
|
-
|
|
51
|
-
// Back-compat helper: read a single named cookie from a Cookie: header
|
|
52
|
-
// string. Delegates to cookies.parse so the parser is one place.
|
|
53
|
-
function _readCookie(cookieHeader, name) {
|
|
54
|
-
if (!cookieHeader || typeof cookieHeader !== "string") return null;
|
|
55
|
-
var jar = cookies.parse(cookieHeader);
|
|
56
|
-
return Object.prototype.hasOwnProperty.call(jar, name) ? jar[name] : null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function _readBearer(authHeader) {
|
|
60
|
-
if (!authHeader || typeof authHeader !== "string") return null;
|
|
61
|
-
// case-insensitive scheme match
|
|
62
|
-
var m = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
63
|
-
return m ? m[1].trim() : null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function create(opts) {
|
|
67
|
-
opts = opts || {};
|
|
68
|
-
validateOpts(opts, [
|
|
69
|
-
"cookieName", "tokenFrom", "sealed", "vault", "userLoader", "audit",
|
|
70
|
-
], "middleware.attachUser");
|
|
71
|
-
if (typeof opts.userLoader !== "function") {
|
|
72
|
-
throw new Error("middleware.attachUser: opts.userLoader is required " +
|
|
73
|
-
"(async function (verifiedSession) → user | null)");
|
|
74
|
-
}
|
|
75
|
-
var cookieName = opts.cookieName || "blamejs_session";
|
|
76
|
-
var tokenFrom = opts.tokenFrom || "both";
|
|
77
|
-
var auditOn = opts.audit !== false;
|
|
78
|
-
var sealed = !!opts.sealed;
|
|
79
|
-
if (sealed && (!opts.vault || typeof opts.vault.unseal !== "function")) {
|
|
80
|
-
throw new Error("middleware.attachUser: opts.sealed requires opts.vault " +
|
|
81
|
-
"with a .unseal method (typically b.vault)");
|
|
82
|
-
}
|
|
83
|
-
// Cookie reader: instance bound to the operator's vault when sealed,
|
|
84
|
-
// otherwise the stateless plain reader.
|
|
85
|
-
var cookieJar = sealed ? cookies.create({ vault: opts.vault }) : null;
|
|
86
|
-
|
|
87
|
-
return async function attachUser(req, res, next) {
|
|
88
|
-
req.user = null;
|
|
89
|
-
req.session = null;
|
|
90
|
-
|
|
91
|
-
var token = null;
|
|
92
|
-
if (tokenFrom === "cookie" || tokenFrom === "both") {
|
|
93
|
-
token = sealed
|
|
94
|
-
? cookieJar.readSealed(req, cookieName)
|
|
95
|
-
: _readCookie(req.headers && req.headers.cookie, cookieName);
|
|
96
|
-
}
|
|
97
|
-
if (!token && (tokenFrom === "header" || tokenFrom === "both")) {
|
|
98
|
-
token = _readBearer(req.headers && req.headers.authorization);
|
|
99
|
-
}
|
|
100
|
-
if (!token) return next();
|
|
101
|
-
|
|
102
|
-
var verified;
|
|
103
|
-
try {
|
|
104
|
-
verified = await session().verify(token);
|
|
105
|
-
} catch (_e) {
|
|
106
|
-
// session.verify is tolerant — shouldn't normally throw, but if it
|
|
107
|
-
// does (DB hiccup), don't propagate; treat as "no user" and let
|
|
108
|
-
// downstream require-auth produce a 401.
|
|
109
|
-
return next();
|
|
110
|
-
}
|
|
111
|
-
if (!verified) return next();
|
|
112
|
-
|
|
113
|
-
var user;
|
|
114
|
-
try {
|
|
115
|
-
user = await opts.userLoader(verified);
|
|
116
|
-
} catch (e) {
|
|
117
|
-
// userLoader threw — treat as "no user" but record so the
|
|
118
|
-
// operator can investigate. Don't surface to the response.
|
|
119
|
-
if (auditOn) {
|
|
120
|
-
try {
|
|
121
|
-
audit().emit({
|
|
122
|
-
action: "auth.session.user_loader_threw",
|
|
123
|
-
outcome: "failure",
|
|
124
|
-
actor: requestHelpers.extractActorContext(req, { userId: verified.userId }),
|
|
125
|
-
reason: (e && e.message) || String(e),
|
|
126
|
-
});
|
|
127
|
-
} catch (_ignored) { /* audit best-effort */ }
|
|
128
|
-
}
|
|
129
|
-
return next();
|
|
130
|
-
}
|
|
131
|
-
if (!user) {
|
|
132
|
-
// Session valid but user record is gone (deleted) or rejected
|
|
133
|
-
// (suspended, etc.). Record + don't attach.
|
|
134
|
-
if (auditOn) {
|
|
135
|
-
try {
|
|
136
|
-
audit().emit({
|
|
137
|
-
action: "auth.session.user_unloadable",
|
|
138
|
-
outcome: "failure",
|
|
139
|
-
actor: requestHelpers.extractActorContext(req, { userId: verified.userId }),
|
|
140
|
-
});
|
|
141
|
-
} catch (_ignored) { /* audit best-effort */ }
|
|
142
|
-
}
|
|
143
|
-
return next();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
req.user = user;
|
|
147
|
-
req.session = verified;
|
|
148
|
-
return next();
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
module.exports = {
|
|
153
|
-
create: create,
|
|
154
|
-
// Exported for tests and operator-side cookie reading
|
|
155
|
-
_readCookie: _readCookie,
|
|
156
|
-
_readBearer: _readBearer,
|
|
157
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* attach-user middleware — populates req.user (and req.session) from a
|
|
4
|
+
* verified session token.
|
|
5
|
+
*
|
|
6
|
+
* Token sources, tried in order:
|
|
7
|
+
* 1. Cookie named opts.cookieName (default "blamejs_session")
|
|
8
|
+
* 2. Authorization: Bearer <token> header
|
|
9
|
+
* The first one that produces a verified session wins. Operators who
|
|
10
|
+
* want only one path can disable the other via opts.tokenFrom.
|
|
11
|
+
*
|
|
12
|
+
* Sealed cookies (access-gated): when opts.sealed is true and opts.vault
|
|
13
|
+
* is provided, the cookie value is read via cookies.readSealed (which
|
|
14
|
+
* vault-unseals and recovers the original session token). Without the
|
|
15
|
+
* framework's vault key no client can hand-craft a valid cookie, so the
|
|
16
|
+
* API is unreachable via curl-with-arbitrary-cookies. Default off for
|
|
17
|
+
* back-compat with apps that put the raw token in the cookie.
|
|
18
|
+
*
|
|
19
|
+
* The framework can't know the application's user schema, so the
|
|
20
|
+
* middleware delegates user-record loading to opts.userLoader — an
|
|
21
|
+
* async function `(verifiedSession) => user | null`. Operators
|
|
22
|
+
* typically look up by userId in their own users table.
|
|
23
|
+
*
|
|
24
|
+
* Failure modes (none of which throw):
|
|
25
|
+
* - No token in either source → req.user = null, next()
|
|
26
|
+
* - Sealed cookie present but unsealable → req.user = null, next()
|
|
27
|
+
* - Token present but session.verify rejects → req.user = null, next()
|
|
28
|
+
* - Session valid but userLoader returns null/undefined → req.user
|
|
29
|
+
* = null, next() + audit emit (with outcome=failure)
|
|
30
|
+
*
|
|
31
|
+
* Always calls next() — the gating decision is downstream's job (use
|
|
32
|
+
* middleware.requireAuth for that). This middleware only ATTACHES.
|
|
33
|
+
*
|
|
34
|
+
* Options:
|
|
35
|
+
* {
|
|
36
|
+
* cookieName: 'blamejs_session' (cookie name to read)
|
|
37
|
+
* tokenFrom: 'both' | 'cookie' | 'header' (default 'both')
|
|
38
|
+
* sealed: false (use cookies.readSealed)
|
|
39
|
+
* vault: b.vault (required when sealed)
|
|
40
|
+
* userLoader: async (verifiedSession) => user (REQUIRED)
|
|
41
|
+
* audit: true (emit user-load audits)
|
|
42
|
+
* }
|
|
43
|
+
*/
|
|
44
|
+
var lazyRequire = require("../lazy-require");
|
|
45
|
+
var cookies = require("../cookies");
|
|
46
|
+
var requestHelpers = require("../request-helpers");
|
|
47
|
+
var validateOpts = require("../validate-opts");
|
|
48
|
+
var session = lazyRequire(function () { return require("../session"); });
|
|
49
|
+
var audit = lazyRequire(function () { return require("../audit"); });
|
|
50
|
+
|
|
51
|
+
// Back-compat helper: read a single named cookie from a Cookie: header
|
|
52
|
+
// string. Delegates to cookies.parse so the parser is one place.
|
|
53
|
+
function _readCookie(cookieHeader, name) {
|
|
54
|
+
if (!cookieHeader || typeof cookieHeader !== "string") return null;
|
|
55
|
+
var jar = cookies.parse(cookieHeader);
|
|
56
|
+
return Object.prototype.hasOwnProperty.call(jar, name) ? jar[name] : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function _readBearer(authHeader) {
|
|
60
|
+
if (!authHeader || typeof authHeader !== "string") return null;
|
|
61
|
+
// case-insensitive scheme match
|
|
62
|
+
var m = authHeader.match(/^Bearer\s+(.+)$/i);
|
|
63
|
+
return m ? m[1].trim() : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function create(opts) {
|
|
67
|
+
opts = opts || {};
|
|
68
|
+
validateOpts(opts, [
|
|
69
|
+
"cookieName", "tokenFrom", "sealed", "vault", "userLoader", "audit",
|
|
70
|
+
], "middleware.attachUser");
|
|
71
|
+
if (typeof opts.userLoader !== "function") {
|
|
72
|
+
throw new Error("middleware.attachUser: opts.userLoader is required " +
|
|
73
|
+
"(async function (verifiedSession) → user | null)");
|
|
74
|
+
}
|
|
75
|
+
var cookieName = opts.cookieName || "blamejs_session";
|
|
76
|
+
var tokenFrom = opts.tokenFrom || "both";
|
|
77
|
+
var auditOn = opts.audit !== false;
|
|
78
|
+
var sealed = !!opts.sealed;
|
|
79
|
+
if (sealed && (!opts.vault || typeof opts.vault.unseal !== "function")) {
|
|
80
|
+
throw new Error("middleware.attachUser: opts.sealed requires opts.vault " +
|
|
81
|
+
"with a .unseal method (typically b.vault)");
|
|
82
|
+
}
|
|
83
|
+
// Cookie reader: instance bound to the operator's vault when sealed,
|
|
84
|
+
// otherwise the stateless plain reader.
|
|
85
|
+
var cookieJar = sealed ? cookies.create({ vault: opts.vault }) : null;
|
|
86
|
+
|
|
87
|
+
return async function attachUser(req, res, next) {
|
|
88
|
+
req.user = null;
|
|
89
|
+
req.session = null;
|
|
90
|
+
|
|
91
|
+
var token = null;
|
|
92
|
+
if (tokenFrom === "cookie" || tokenFrom === "both") {
|
|
93
|
+
token = sealed
|
|
94
|
+
? cookieJar.readSealed(req, cookieName)
|
|
95
|
+
: _readCookie(req.headers && req.headers.cookie, cookieName);
|
|
96
|
+
}
|
|
97
|
+
if (!token && (tokenFrom === "header" || tokenFrom === "both")) {
|
|
98
|
+
token = _readBearer(req.headers && req.headers.authorization);
|
|
99
|
+
}
|
|
100
|
+
if (!token) return next();
|
|
101
|
+
|
|
102
|
+
var verified;
|
|
103
|
+
try {
|
|
104
|
+
verified = await session().verify(token);
|
|
105
|
+
} catch (_e) {
|
|
106
|
+
// session.verify is tolerant — shouldn't normally throw, but if it
|
|
107
|
+
// does (DB hiccup), don't propagate; treat as "no user" and let
|
|
108
|
+
// downstream require-auth produce a 401.
|
|
109
|
+
return next();
|
|
110
|
+
}
|
|
111
|
+
if (!verified) return next();
|
|
112
|
+
|
|
113
|
+
var user;
|
|
114
|
+
try {
|
|
115
|
+
user = await opts.userLoader(verified);
|
|
116
|
+
} catch (e) {
|
|
117
|
+
// userLoader threw — treat as "no user" but record so the
|
|
118
|
+
// operator can investigate. Don't surface to the response.
|
|
119
|
+
if (auditOn) {
|
|
120
|
+
try {
|
|
121
|
+
audit().emit({
|
|
122
|
+
action: "auth.session.user_loader_threw",
|
|
123
|
+
outcome: "failure",
|
|
124
|
+
actor: requestHelpers.extractActorContext(req, { userId: verified.userId }),
|
|
125
|
+
reason: (e && e.message) || String(e),
|
|
126
|
+
});
|
|
127
|
+
} catch (_ignored) { /* audit best-effort */ }
|
|
128
|
+
}
|
|
129
|
+
return next();
|
|
130
|
+
}
|
|
131
|
+
if (!user) {
|
|
132
|
+
// Session valid but user record is gone (deleted) or rejected
|
|
133
|
+
// (suspended, etc.). Record + don't attach.
|
|
134
|
+
if (auditOn) {
|
|
135
|
+
try {
|
|
136
|
+
audit().emit({
|
|
137
|
+
action: "auth.session.user_unloadable",
|
|
138
|
+
outcome: "failure",
|
|
139
|
+
actor: requestHelpers.extractActorContext(req, { userId: verified.userId }),
|
|
140
|
+
});
|
|
141
|
+
} catch (_ignored) { /* audit best-effort */ }
|
|
142
|
+
}
|
|
143
|
+
return next();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
req.user = user;
|
|
147
|
+
req.session = verified;
|
|
148
|
+
return next();
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
create: create,
|
|
154
|
+
// Exported for tests and operator-side cookie reading
|
|
155
|
+
_readCookie: _readCookie,
|
|
156
|
+
_readBearer: _readBearer,
|
|
157
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* bearer-auth middleware — extracts `Authorization: Bearer <token>`,
|
|
4
|
+
* runs an operator-supplied verifier, and attaches the result to
|
|
5
|
+
* `req.user`. Distinct from `attachUser` (which reads session cookies)
|
|
6
|
+
* — this is the API-token / JWT / OAuth-access-token path.
|
|
7
|
+
*
|
|
8
|
+
* Mount this for routes that accept bearer tokens. Operators that
|
|
9
|
+
* accept BOTH cookie sessions AND bearer tokens mount both: bearerAuth
|
|
10
|
+
* runs first; if no Bearer header, it calls next() so attachUser /
|
|
11
|
+
* requireAuth can take over. If a Bearer header IS present but invalid,
|
|
12
|
+
* bearerAuth rejects with 401 immediately (avoids the
|
|
13
|
+
* "is this a bearer or a cookie session?" collision in attach-user.js).
|
|
14
|
+
*
|
|
15
|
+
* var bearer = b.middleware.bearerAuth({
|
|
16
|
+
* verify: async function (token) {
|
|
17
|
+
* // operator-supplied: return a user object or null/throw
|
|
18
|
+
* var rec = await b.apiKey.verify(token);
|
|
19
|
+
* return rec ? { id: rec.ownerId, scopes: rec.scopes } : null;
|
|
20
|
+
* },
|
|
21
|
+
* audit: true, // default
|
|
22
|
+
* scheme: "Bearer", // default; some ops use "Token"
|
|
23
|
+
* errorMessage: "Bearer token required.",
|
|
24
|
+
* });
|
|
25
|
+
* router.use("/api", bearer);
|
|
26
|
+
*
|
|
27
|
+
* Verify result shape:
|
|
28
|
+
* - object truthy → req.user = result; next()
|
|
29
|
+
* - null / undefined / false → 401 (token invalid)
|
|
30
|
+
* - throws an Error with .code === "auth-bearer/expired" → 401 + WWW-Authenticate
|
|
31
|
+
*
|
|
32
|
+
* Audit: `auth.bearer.success` on accept; `auth.bearer.failure` with
|
|
33
|
+
* reason on reject. Both carry actor context (clientIp, userAgent,
|
|
34
|
+
* route).
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
var lazyRequire = require("../lazy-require");
|
|
38
|
+
var requestHelpers = require("../request-helpers");
|
|
39
|
+
var validateOpts = require("../validate-opts");
|
|
40
|
+
var { AuthError } = require("../framework-error");
|
|
41
|
+
|
|
42
|
+
var audit = lazyRequire(function () { return require("../audit"); });
|
|
43
|
+
var observability = lazyRequire(function () { return require("../observability"); });
|
|
44
|
+
|
|
45
|
+
function _writeUnauthorized(res, scheme, message, realm) {
|
|
46
|
+
if (res.headersSent) return;
|
|
47
|
+
var body = JSON.stringify({ error: message });
|
|
48
|
+
var challenge = scheme + (realm ? ' realm="' + realm + '"' : "");
|
|
49
|
+
res.writeHead(401, { // allow:raw-byte-literal — HTTP 401 status
|
|
50
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
51
|
+
"Content-Length": Buffer.byteLength(body),
|
|
52
|
+
"WWW-Authenticate": challenge,
|
|
53
|
+
});
|
|
54
|
+
res.end(body);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function _extractToken(req, scheme) {
|
|
58
|
+
var h = req.headers && req.headers.authorization;
|
|
59
|
+
if (typeof h !== "string" || h.length === 0) return null;
|
|
60
|
+
var prefix = scheme + " ";
|
|
61
|
+
if (h.length <= prefix.length) return null;
|
|
62
|
+
if (h.slice(0, prefix.length).toLowerCase() !== prefix.toLowerCase()) return null;
|
|
63
|
+
var token = h.slice(prefix.length).trim();
|
|
64
|
+
return token.length > 0 ? token : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function create(opts) {
|
|
68
|
+
opts = opts || {};
|
|
69
|
+
validateOpts(opts, [
|
|
70
|
+
"verify", "audit", "scheme", "errorMessage", "realm",
|
|
71
|
+
"tokenAttachKey", "userAttachKey",
|
|
72
|
+
], "middleware.bearerAuth");
|
|
73
|
+
|
|
74
|
+
if (typeof opts.verify !== "function") {
|
|
75
|
+
throw new AuthError("auth-bearer/missing-verify",
|
|
76
|
+
"middleware.bearerAuth requires a verify(token) function — operators MUST supply " +
|
|
77
|
+
"the verification path (b.apiKey.verify / b.auth.jwt.verifyExternal / custom)");
|
|
78
|
+
}
|
|
79
|
+
var auditOn = opts.audit !== false;
|
|
80
|
+
var scheme = opts.scheme || "Bearer";
|
|
81
|
+
var errorMessage = opts.errorMessage || "Bearer token required.";
|
|
82
|
+
var realm = opts.realm || null;
|
|
83
|
+
var tokenAttach = opts.tokenAttachKey || "bearerToken";
|
|
84
|
+
var userAttach = opts.userAttachKey || "user";
|
|
85
|
+
|
|
86
|
+
function _emitAudit(action, outcome, req, reason) {
|
|
87
|
+
if (!auditOn) return;
|
|
88
|
+
try {
|
|
89
|
+
var actor = requestHelpers.extractActorContext(req);
|
|
90
|
+
audit().safeEmit({
|
|
91
|
+
action: action, outcome: outcome,
|
|
92
|
+
metadata: Object.assign({}, actor, {
|
|
93
|
+
route: req.url,
|
|
94
|
+
method: req.method,
|
|
95
|
+
reason: reason || null,
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
} catch (_e) { /* audit best-effort */ }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function _emitObs(metric, n, tags) {
|
|
102
|
+
try { observability().count(metric, n, tags || {}); }
|
|
103
|
+
catch (_e) { /* best-effort */ }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return async function bearerAuth(req, res, next) {
|
|
107
|
+
var token = _extractToken(req, scheme);
|
|
108
|
+
if (!token) {
|
|
109
|
+
// No Bearer header — fall through. Cookie-based session middleware
|
|
110
|
+
// running after this can attach a user via the cookie path.
|
|
111
|
+
return next();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
var user;
|
|
115
|
+
try {
|
|
116
|
+
user = await opts.verify(token);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
var code = (e && e.code) || "auth-bearer/verify-failed";
|
|
119
|
+
_emitAudit("auth.bearer.failure", "failure", req, code);
|
|
120
|
+
_emitObs("auth.bearer.rejected", 1, { reason: code });
|
|
121
|
+
// Per RFC 6750 §3 — `Bearer error="invalid_token"` is the
|
|
122
|
+
// standardized challenge for verifier-rejected tokens.
|
|
123
|
+
var challenge = scheme + ' error="invalid_token"' +
|
|
124
|
+
(realm ? ', realm="' + realm + '"' : "");
|
|
125
|
+
if (!res.headersSent) {
|
|
126
|
+
var body = JSON.stringify({ error: errorMessage });
|
|
127
|
+
res.writeHead(401, { // allow:raw-byte-literal — HTTP 401 status
|
|
128
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
129
|
+
"Content-Length": Buffer.byteLength(body),
|
|
130
|
+
"WWW-Authenticate": challenge,
|
|
131
|
+
});
|
|
132
|
+
res.end(body);
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!user) {
|
|
138
|
+
_emitAudit("auth.bearer.failure", "failure", req, "verifier-returned-null");
|
|
139
|
+
_emitObs("auth.bearer.rejected", 1, { reason: "verifier-null" });
|
|
140
|
+
_writeUnauthorized(res, scheme, errorMessage, realm);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
req[tokenAttach] = token;
|
|
145
|
+
req[userAttach] = user;
|
|
146
|
+
_emitAudit("auth.bearer.success", "success", req, null);
|
|
147
|
+
_emitObs("auth.bearer.accepted", 1, {});
|
|
148
|
+
next();
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = { create: create };
|