From 3e862d5a4aaacd80a2da7b37bbe7c7179d1a9731 Mon Sep 17 00:00:00 2001 From: cstns Date: Fri, 16 Oct 2026 13:30:12 +0300 Subject: [PATCH] Recheck accepted HTTP bearer tokens after 60 seconds instead of 5 minutes Accepted bearer tokens were trusted for 300 seconds before the platform was asked again, so a deleted or expired token kept working for up to 5 minutes. The window is now 60 seconds. --- .../flowfuse-auth/httpAuthMiddleware.js | 5 +- .../flowfuseAuthHttpMiddleware_spec.js | 64 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 test/unit/lib/plugins/flowfuseAuthHttpMiddleware_spec.js diff --git a/lib/plugins/node_modules/@flowfuse/flowfuse-auth/httpAuthMiddleware.js b/lib/plugins/node_modules/@flowfuse/flowfuse-auth/httpAuthMiddleware.js index ae87222c..666a1e8a 100644 --- a/lib/plugins/node_modules/@flowfuse/flowfuse-auth/httpAuthMiddleware.js +++ b/lib/plugins/node_modules/@flowfuse/flowfuse-auth/httpAuthMiddleware.js @@ -10,6 +10,9 @@ let passport let httpNodeApp let client const httpTokenCache = {} +// How long an accepted bearer token is trusted before it is checked with the platform again, +// which is also how long a deleted or expired token can keep working +const HTTP_TOKEN_CACHE_SECONDS = 60 module.exports = { init (_options) { @@ -26,7 +29,7 @@ module.exports = { const cacheHit = httpTokenCache[token] if (cacheHit) { const age = (Date.now() - cacheHit.age) / 1000 - if (age < 300) { + if (age < HTTP_TOKEN_CACHE_SECONDS) { next() return } diff --git a/test/unit/lib/plugins/flowfuseAuthHttpMiddleware_spec.js b/test/unit/lib/plugins/flowfuseAuthHttpMiddleware_spec.js new file mode 100644 index 00000000..49b8f804 --- /dev/null +++ b/test/unit/lib/plugins/flowfuseAuthHttpMiddleware_spec.js @@ -0,0 +1,64 @@ +const should = require('should') +const http = require('http') +const sinon = require('sinon') + +const httpAuth = require('../../../../lib/plugins/node_modules/@flowfuse/flowfuse-auth/httpAuthMiddleware') + +describe('FlowFuse auth plugin bearer token cache', function () { + let forge, tokenChecks, clock, authMiddleware + + beforeEach(async function () { + // A stand-in for the platform's token check endpoint that counts how often it is asked + tokenChecks = 0 + forge = http.createServer((req, res) => { + if (req.url.startsWith('/account/check/http:device/device-1')) { + tokenChecks++ + } + res.setHeader('content-type', 'application/json') + res.end('{}') + }) + forge.listen(0) + await new Promise(resolve => forge.once('listening', resolve)) + + authMiddleware = httpAuth.init({ + baseURL: 'http://127.0.0.1', + forgeURL: `http://127.0.0.1:${forge.address().port}`, + clientID: 'client-id', + clientSecret: 'client-secret', + teamID: 'team-1', + deviceId: 'device-1' + })[0] + // Only the token check client matters here, so the routes go to a stub app + httpAuth.setupAuthRoutes({ use () {}, get () {} }) + + // Only fake Date, so the server and sockets keep working + clock = sinon.useFakeTimers({ now: Date.now(), toFake: ['Date'] }) + }) + + afterEach(function () { + clock.restore() + forge.close() + }) + + // Runs one request carrying the bearer token through the middleware and reports whether it got through + async function request () { + const req = { session: {}, path: '/endpoint', get: (name) => name === 'Authorization' ? 'Bearer cache-test-token' : undefined } + const next = sinon.spy() + await authMiddleware(req, {}, next) + next.calledOnce.should.be.true() + return next.firstCall.args[0] + } + + it('rechecks an accepted token with the platform once it is a minute old', async function () { + should(await request()).be.undefined() + tokenChecks.should.equal(1) + + clock.tick(59 * 1000) + should(await request()).be.undefined() + tokenChecks.should.equal(1) + + clock.tick(2 * 1000) + should(await request()).be.undefined() + tokenChecks.should.equal(2) + }) +})