From d933c8fb50f60e50a164d6d2f57f3a66252cd830 Mon Sep 17 00:00:00 2001 From: Caesar Mukama Date: Tue, 14 Apr 2026 21:57:01 +0300 Subject: [PATCH 1/4] feat: adopt JWT-based svc-facs-auth (TDEBT-13) Tokens coming from svc-facs-auth are now HS256-signed JWTs. Adds jwtSecret and lowers ttl to 900s in the config example so the app-node LRU-backed jti denylist (15-min maxAge) fully covers any revocation window. Inlines the permission check in AuthLib via a new _permsMatch helper so a single call collapses from ~4 JWT verifications per write request to 1. --- config/facs/auth.config.json.example | 3 ++- tests/unit/lib/auth.test.js | 25 +++++++------------------ workers/lib/auth.js | 18 ++++++++++++------ 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/config/facs/auth.config.json.example b/config/facs/auth.config.json.example index 3e72947..2e4af13 100644 --- a/config/facs/auth.config.json.example +++ b/config/facs/auth.config.json.example @@ -1,7 +1,8 @@ { "a0": { "superAdmin": "test@localhost", - "ttl": 86400, + "ttl": 900, + "jwtSecret": "REPLACE_WITH_LONG_RANDOM_HEX_SECRET", "saltRounds": 10, "superAdminPerms": [ "miner:rw", diff --git a/tests/unit/lib/auth.test.js b/tests/unit/lib/auth.test.js index 00b852d..d775515 100644 --- a/tests/unit/lib/auth.test.js +++ b/tests/unit/lib/auth.test.js @@ -231,10 +231,7 @@ test('AuthLib - getTokenPerms with super admin', async (t) => { test('AuthLib - getTokenPerms with regular user', async (t) => { const mockAuth = { getTokenPerms: function (token) { - return { superadmin: false, perms: ['actions:r', 'miner:r'] } - }, - tokenHasPerms: async (token, perm) => { - return perm === 'actions:w' + return { superadmin: false, perms: ['actions:rw', 'miner:r'] } }, conf: { superAdminPerms: [] @@ -262,7 +259,6 @@ test('AuthLib - tokenHasPerms with super admin', async (t) => { getTokenPerms: function () { return { superadmin: true, perms: [] } }, - tokenHasPerms: async () => false, conf: { superAdminPerms: [] } @@ -274,7 +270,7 @@ test('AuthLib - tokenHasPerms with super admin', async (t) => { auth: mockAuth }) - const result = await authLib.tokenHasPerms('token', true, ['perm1', 'perm2']) + const result = await authLib.tokenHasPerms('token', true, ['perm1:r', 'perm2:r']) t.is(result, true, 'should return true for super admin') @@ -286,7 +282,6 @@ test('AuthLib - tokenHasPerms without write permission', async (t) => { getTokenPerms: function () { return { superadmin: false, perms: [] } }, - tokenHasPerms: async () => false, conf: { superAdminPerms: [] } @@ -298,7 +293,7 @@ test('AuthLib - tokenHasPerms without write permission', async (t) => { auth: mockAuth }) - const result = await authLib.tokenHasPerms('token', true, ['perm1']) + const result = await authLib.tokenHasPerms('token', true, ['perm1:r']) t.is(result, false, 'should return false when write required but not available') @@ -308,10 +303,7 @@ test('AuthLib - tokenHasPerms without write permission', async (t) => { test('AuthLib - tokenHasPerms with matchAll=true', async (t) => { const mockAuth = { getTokenPerms: function () { - return { superadmin: false, perms: [] } - }, - tokenHasPerms: async (token, perm) => { - return perm === 'perm1' + return { superadmin: false, perms: ['perm1:r'] } }, conf: { superAdminPerms: [] @@ -324,7 +316,7 @@ test('AuthLib - tokenHasPerms with matchAll=true', async (t) => { auth: mockAuth }) - const result = await authLib.tokenHasPerms('token', false, ['perm1', 'perm2'], true) + const result = await authLib.tokenHasPerms('token', false, ['perm1:r', 'perm2:r'], true) t.is(result, false, 'should return false when matchAll and not all perms match') @@ -334,10 +326,7 @@ test('AuthLib - tokenHasPerms with matchAll=true', async (t) => { test('AuthLib - tokenHasPerms with matchAll=false', async (t) => { const mockAuth = { getTokenPerms: function () { - return { superadmin: false, perms: [] } - }, - tokenHasPerms: async (token, perm) => { - return perm === 'perm1' + return { superadmin: false, perms: ['perm1:r'] } }, conf: { superAdminPerms: [] @@ -350,7 +339,7 @@ test('AuthLib - tokenHasPerms with matchAll=false', async (t) => { auth: mockAuth }) - const result = await authLib.tokenHasPerms('token', false, ['perm1', 'perm2'], false) + const result = await authLib.tokenHasPerms('token', false, ['perm1:r', 'perm2:r'], false) t.is(result, true, 'should return true when matchAll=false and at least one perm matches') diff --git a/workers/lib/auth.js b/workers/lib/auth.js index 1b299dc..efb37fa 100644 --- a/workers/lib/auth.js +++ b/workers/lib/auth.js @@ -1,6 +1,12 @@ 'use strict' const { SUPER_ADMIN_ID, MIGRATED_USER_ROLES } = require('./constants') +function _permsMatch (perms, perm) { + const [key, required] = perm.split(':') + const av = perms.find(p => p.startsWith(`${key}:`))?.split(':')[1] ?? '' + return [...required].every(c => av.includes(c)) +} + class AuthLib { constructor ({ httpc, httpd, userService, auth }) { this._httpc = httpc @@ -63,8 +69,8 @@ class AuthLib { } async getTokenPerms (token) { - const { superadmin: superAdmin, perms = [] } = this._auth.getTokenPerms(token) - const write = superAdmin || (await this._auth.tokenHasPerms(token, 'actions:w')) + const { superadmin: superAdmin, perms = [] } = await this._auth.getTokenPerms(token) + const write = superAdmin || _permsMatch(perms, 'actions:w') const applicablePerms = superAdmin ? (this._auth.conf.superAdminPerms ?? []) : perms const caps = applicablePerms.map(perm => perm.split(':')[0]) @@ -72,16 +78,16 @@ class AuthLib { } async tokenHasPerms (token, write, requestedPerms, matchAll = false) { - const perms = await this.getTokenPerms(token) - if (perms.superAdmin) { + const { superAdmin, write: hasWrite, permissions } = await this.getTokenPerms(token) + if (superAdmin) { return true } - if (write && !perms.write) { + if (write && !hasWrite) { return false } - const resolved = await Promise.all(requestedPerms.map(perm => this._auth.tokenHasPerms(token, perm))) + const resolved = requestedPerms.map(perm => _permsMatch(permissions, perm)) return matchAll ? resolved.every(res => res) From d8bfcae85add3538ce26eb7eaa056b695f83ea8e Mon Sep 17 00:00:00 2001 From: Caesar Mukama Date: Wed, 15 Apr 2026 16:35:59 +0300 Subject: [PATCH 2/4] chore: drop redundant await on _auth.getTokenPerms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit svc-facs-auth#17 keeps the facility's getTokenPerms/tokenHasPerms synchronous in both legacy and JWT modes for backward compat. The await added in the earlier commit was harmless (await of a value is a no-op) but no longer needed — reverting it brings the line back to the main baseline. --- workers/lib/auth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workers/lib/auth.js b/workers/lib/auth.js index efb37fa..5c8555c 100644 --- a/workers/lib/auth.js +++ b/workers/lib/auth.js @@ -69,7 +69,7 @@ class AuthLib { } async getTokenPerms (token) { - const { superadmin: superAdmin, perms = [] } = await this._auth.getTokenPerms(token) + const { superadmin: superAdmin, perms = [] } = this._auth.getTokenPerms(token) const write = superAdmin || _permsMatch(perms, 'actions:w') const applicablePerms = superAdmin ? (this._auth.conf.superAdminPerms ?? []) : perms const caps = applicablePerms.map(perm => perm.split(':')[0]) From 41d342a3ee7360e8e6493a822b654c3b19dc771c Mon Sep 17 00:00:00 2001 From: Caesar Mukama Date: Wed, 15 Apr 2026 17:16:53 +0300 Subject: [PATCH 3/4] refactor: keep original perms binding in tokenHasPerms Reverts the { superAdmin, write: hasWrite, permissions } destructure rename back to const perms = await this.getTokenPerms(token) from main. Access via perms.superAdmin / perms.write / perms.permissions matches the original style. No behavioural change. --- workers/lib/auth.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/workers/lib/auth.js b/workers/lib/auth.js index 5c8555c..e2299ca 100644 --- a/workers/lib/auth.js +++ b/workers/lib/auth.js @@ -78,16 +78,16 @@ class AuthLib { } async tokenHasPerms (token, write, requestedPerms, matchAll = false) { - const { superAdmin, write: hasWrite, permissions } = await this.getTokenPerms(token) - if (superAdmin) { + const perms = await this.getTokenPerms(token) + if (perms.superAdmin) { return true } - if (write && !hasWrite) { + if (write && !perms.write) { return false } - const resolved = requestedPerms.map(perm => _permsMatch(permissions, perm)) + const resolved = requestedPerms.map(perm => _permsMatch(perms.permissions, perm)) return matchAll ? resolved.every(res => res) From d3d30f2e6abf124c8142966088334f726b209e7c Mon Sep 17 00:00:00 2001 From: Caesar Mukama Date: Wed, 6 May 2026 21:17:47 +0300 Subject: [PATCH 4/4] refactor: move _permsMatch into AuthLib class --- workers/lib/auth.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/workers/lib/auth.js b/workers/lib/auth.js index e2299ca..01a94d3 100644 --- a/workers/lib/auth.js +++ b/workers/lib/auth.js @@ -1,12 +1,6 @@ 'use strict' const { SUPER_ADMIN_ID, MIGRATED_USER_ROLES } = require('./constants') -function _permsMatch (perms, perm) { - const [key, required] = perm.split(':') - const av = perms.find(p => p.startsWith(`${key}:`))?.split(':')[1] ?? '' - return [...required].every(c => av.includes(c)) -} - class AuthLib { constructor ({ httpc, httpd, userService, auth }) { this._httpc = httpc @@ -15,6 +9,12 @@ class AuthLib { this._auth = auth } + _permsMatch (perms, perm) { + const [key, required] = perm.split(':') + const av = perms.find(p => p.startsWith(`${key}:`))?.split(':')[1] ?? '' + return [...required].every(c => av.includes(c)) + } + async migrateUsers (httpdAuth) { const users = await this._auth.listUsers() if (users.length > 1) { @@ -70,7 +70,7 @@ class AuthLib { async getTokenPerms (token) { const { superadmin: superAdmin, perms = [] } = this._auth.getTokenPerms(token) - const write = superAdmin || _permsMatch(perms, 'actions:w') + const write = superAdmin || this._permsMatch(perms, 'actions:w') const applicablePerms = superAdmin ? (this._auth.conf.superAdminPerms ?? []) : perms const caps = applicablePerms.map(perm => perm.split(':')[0]) @@ -87,7 +87,7 @@ class AuthLib { return false } - const resolved = requestedPerms.map(perm => _permsMatch(perms.permissions, perm)) + const resolved = requestedPerms.map(perm => this._permsMatch(perms.permissions, perm)) return matchAll ? resolved.every(res => res)