@onlineapps/infrastructure-tools 1.1.4 → 1.1.5
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/package.json +2 -2
- package/src/jwt/createJwtValidator.js +30 -3
- package/src/jwt/extractTenantContext.js +16 -10
- package/src/jwt/index.js +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onlineapps/infrastructure-tools",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"description": "Infrastructure orchestration utilities for OA Drive infrastructure services (health tracking, queue initialization, service discovery)",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@onlineapps/infra-logger": "1.0.0",
|
|
22
|
-
"@onlineapps/mq-client-core": "1.0.
|
|
22
|
+
"@onlineapps/mq-client-core": "1.0.83",
|
|
23
23
|
"@onlineapps/service-common": "1.0.18",
|
|
24
24
|
"@onlineapps/storage-core": "1.0.12",
|
|
25
25
|
"jsonwebtoken": "^9.0.3",
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const { verifyAccessToken } = require('./verifyAccessToken');
|
|
4
4
|
|
|
5
|
+
// See: docs/standards/JWT_AUTH.md §9.1 — forced refresh on role change
|
|
6
|
+
const ROLES_VERSION_PREFIX = 'person:roles_version:';
|
|
7
|
+
|
|
5
8
|
/**
|
|
6
9
|
* Create Express middleware that validates JWT Bearer tokens on incoming requests.
|
|
7
10
|
*
|
|
@@ -12,16 +15,17 @@ const { verifyAccessToken } = require('./verifyAccessToken');
|
|
|
12
15
|
* @param {object} options
|
|
13
16
|
* @param {object} options.logger - Logger with .warn() method (required)
|
|
14
17
|
* @param {string[]} [options.excludePaths] - Paths to skip JWT validation
|
|
18
|
+
* @param {object} [options.redisClient] - Redis client for role version check (optional)
|
|
15
19
|
* @returns {Function} Express middleware (req, res, next)
|
|
16
20
|
*/
|
|
17
|
-
function createJwtValidator({ logger, excludePaths }) {
|
|
21
|
+
function createJwtValidator({ logger, excludePaths, redisClient }) {
|
|
18
22
|
if (!logger || typeof logger.warn !== 'function') {
|
|
19
23
|
throw new Error('[JWT] Logger is required - Expected object with warn() method');
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
const excluded = new Set(excludePaths || []);
|
|
23
27
|
|
|
24
|
-
return function jwtValidator(req, res, next) {
|
|
28
|
+
return async function jwtValidator(req, res, next) {
|
|
25
29
|
if (excluded.has(req.path)) {
|
|
26
30
|
return next();
|
|
27
31
|
}
|
|
@@ -38,6 +42,29 @@ function createJwtValidator({ logger, excludePaths }) {
|
|
|
38
42
|
try {
|
|
39
43
|
const decoded = verifyAccessToken(token);
|
|
40
44
|
|
|
45
|
+
if (redisClient && redisClient.isOpen && decoded.person_id) {
|
|
46
|
+
try {
|
|
47
|
+
const rolesVersion = await redisClient.get(`${ROLES_VERSION_PREFIX}${decoded.person_id}`);
|
|
48
|
+
if (rolesVersion) {
|
|
49
|
+
const versionTs = parseInt(rolesVersion, 10);
|
|
50
|
+
const tokenIat = decoded.iat * 1000;
|
|
51
|
+
if (!isNaN(versionTs) && tokenIat < versionTs) {
|
|
52
|
+
logger.warn('[JWT] Token stale — role changed after issuance', {
|
|
53
|
+
person_id: decoded.person_id,
|
|
54
|
+
token_iat: new Date(tokenIat).toISOString(),
|
|
55
|
+
roles_changed: new Date(versionTs).toISOString()
|
|
56
|
+
});
|
|
57
|
+
return res.status(401).json({
|
|
58
|
+
error: 'Token stale — your roles have changed, please refresh your access token',
|
|
59
|
+
code: 'TOKEN_STALE'
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (redisErr) {
|
|
64
|
+
logger.warn('[JWT] Redis role version check failed (non-blocking)', { error: redisErr.message });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
41
68
|
req.auth = {
|
|
42
69
|
person_uuid: decoded.sub,
|
|
43
70
|
person_id: decoded.person_id,
|
|
@@ -59,4 +86,4 @@ function createJwtValidator({ logger, excludePaths }) {
|
|
|
59
86
|
};
|
|
60
87
|
}
|
|
61
88
|
|
|
62
|
-
module.exports = { createJwtValidator };
|
|
89
|
+
module.exports = { createJwtValidator, ROLES_VERSION_PREFIX };
|
|
@@ -9,15 +9,19 @@
|
|
|
9
9
|
* Rules (per JWT_AUTH.md section 7):
|
|
10
10
|
* - Single tenant: auto-pick, x-tenant-uuid header optional
|
|
11
11
|
* - Multiple tenants: x-tenant-uuid header required
|
|
12
|
-
* - workspace_id
|
|
12
|
+
* - workspace_id: required for direct API calls (x-workspace-id header),
|
|
13
|
+
* optional for workflow submission (workspace is per-step in cookbook)
|
|
13
14
|
*
|
|
14
15
|
* @param {object} auth - req.auth from createJwtValidator: { person_uuid, person_id, email, tenants }
|
|
15
16
|
* @param {object} headers - HTTP request headers (lowercase keys)
|
|
17
|
+
* @param {object} [options] - { requireWorkspace: true } — set false for workflow submission
|
|
16
18
|
* @returns {object} { tenant_id, tenant_uuid, workspace_id, person_id, person_uuid, role }
|
|
17
19
|
* @throws {Error} with .statusCode = 400, 401, or 403
|
|
18
20
|
*/
|
|
19
21
|
// See: docs/standards/tenant-context-contract.md
|
|
20
|
-
function extractTenantContext(auth, headers) {
|
|
22
|
+
function extractTenantContext(auth, headers, options) {
|
|
23
|
+
const { requireWorkspace = true } = options || {};
|
|
24
|
+
|
|
21
25
|
if (!auth || !Array.isArray(auth.tenants)) {
|
|
22
26
|
const err = new Error('[TenantContext] Missing auth data - Expected auth object with tenants array');
|
|
23
27
|
err.statusCode = 401;
|
|
@@ -27,14 +31,16 @@ function extractTenantContext(auth, headers) {
|
|
|
27
31
|
const tenantUuid = headers['x-tenant-uuid'];
|
|
28
32
|
const rawWorkspaceId = headers['x-workspace-id'];
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
let workspaceId = null;
|
|
35
|
+
if (rawWorkspaceId) {
|
|
36
|
+
workspaceId = parseInt(rawWorkspaceId, 10);
|
|
37
|
+
if (isNaN(workspaceId) || workspaceId < 1) {
|
|
38
|
+
const err = new Error(`[TenantContext] Invalid x-workspace-id '${rawWorkspaceId}' - must be a positive integer`);
|
|
39
|
+
err.statusCode = 400;
|
|
40
|
+
throw err;
|
|
41
|
+
}
|
|
42
|
+
} else if (requireWorkspace) {
|
|
43
|
+
const err = new Error('[TenantContext] Missing x-workspace-id header - workspace is required for direct API calls');
|
|
38
44
|
err.statusCode = 400;
|
|
39
45
|
throw err;
|
|
40
46
|
}
|
package/src/jwt/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { verifyAccessToken } = require('./verifyAccessToken');
|
|
4
|
-
const { createJwtValidator } = require('./createJwtValidator');
|
|
4
|
+
const { createJwtValidator, ROLES_VERSION_PREFIX } = require('./createJwtValidator');
|
|
5
5
|
const { extractTenantContext } = require('./extractTenantContext');
|
|
6
6
|
|
|
7
7
|
module.exports = {
|
|
8
8
|
verifyAccessToken,
|
|
9
9
|
createJwtValidator,
|
|
10
|
-
extractTenantContext
|
|
10
|
+
extractTenantContext,
|
|
11
|
+
ROLES_VERSION_PREFIX
|
|
11
12
|
};
|