From 8dc2d095729f5bbcc71c29a1690a62e3ce686834 Mon Sep 17 00:00:00 2001 From: Dan Bruce Date: Fri, 9 Jan 2026 13:28:41 -0500 Subject: [PATCH 1/3] attempt refresh-token fix --- src/auth.js | 69 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 16 deletions(-) diff --git a/src/auth.js b/src/auth.js index e03f524..6d1a6c4 100644 --- a/src/auth.js +++ b/src/auth.js @@ -121,7 +121,10 @@ async function isLoggedIn(req, res, next) { // Prefer the already-decoded accessClaims stored at login let accessClaims = req.session.accessClaims || {}; - // Try to verify the access token via JWKS (fast, no client secret required) + // Try to verify the access token via JWKS (fast, no client secret required). + // If verification fails (commonly because the access token expired), attempt + // to refresh using the stored refresh_token. Only destroy the session if + // refresh fails or introspection explicitly reports inactive. try { const oidcCfg = await initOidc(); const JWKS = createRemoteJWKSet(new URL(oidcCfg.jwksUri)); @@ -132,26 +135,60 @@ async function isLoggedIn(req, res, next) { accessClaims = payload || accessClaims; } } catch (e) { - // If jwt verification fails, don't immediately destroy session. - // If introspection is available (and client secret configured), use it to check active status. + // Verification failed; try to refresh the token if we have a refresh_token try { - if (process.env.KEYCLOAK_CLIENT_SECRET && typeof oidcClient.introspect === 'function') { - const introspectResp = await oidcClient.introspect(accessToken); - if (!introspectResp || introspectResp.active !== true) { - try { req.session.destroy(() => {}); } catch (e2) {} - return res.status(401).send({ code: 401, error: 'Unauthorized' }); + const tokenSet = req.session && req.session.tokenSet; + if (tokenSet && tokenSet.refresh_token && typeof oidcClient.refresh === 'function') { + // Attempt to refresh + const newTokenSet = await oidcClient.refresh(tokenSet.refresh_token); + // Persist refreshed tokens in session + req.session.tokenSet = newTokenSet; + try { req.session.claims = newTokenSet.claims(); } catch (e2) {} + + // Try to verify the new access token and set accessClaims + try { + const oidcCfg2 = await initOidc(); + const JWKS2 = createRemoteJWKSet(new URL(oidcCfg2.jwksUri)); + const verifyOpts2 = { issuer: oidcCfg2.issuerUrl }; + if (process.env.KEYCLOAK_CLIENT) verifyOpts2.audience = process.env.KEYCLOAK_CLIENT; + if (newTokenSet && newTokenSet.access_token) { + const { payload } = await jwtVerify(newTokenSet.access_token, JWKS2, verifyOpts2); + accessClaims = payload || accessClaims; + req.session.accessClaims = accessClaims; + } + } catch (e3) { + // Fallback: decode without verification + try { + const { decodeJwt } = require('jose'); + if (req.session && req.session.tokenSet && req.session.tokenSet.access_token) { + req.session.accessClaims = decodeJwt(req.session.tokenSet.access_token); + accessClaims = req.session.accessClaims; + } + } catch (e4) { + // ignore + } } } else { - // As a last resort, try userinfo but do not destroy session if it fails; fall back to stored claims - try { - const userinfo = await oidcClient.userinfo(accessToken); - if (userinfo) accessClaims = userinfo; - } catch (e2) { - // ignore; we'll use whatever we have in session + // No refresh available; fallback to introspection if possible or userinfo + if (process.env.KEYCLOAK_CLIENT_SECRET && typeof oidcClient.introspect === 'function') { + const introspectResp = await oidcClient.introspect(accessToken); + if (!introspectResp || introspectResp.active !== true) { + try { req.session.destroy(() => {}); } catch (e2) {} + return res.status(401).send({ code: 401, error: 'Unauthorized' }); + } + } else { + try { + const userinfo = await oidcClient.userinfo(accessToken); + if (userinfo) accessClaims = userinfo; + } catch (e2) { + // ignore; we'll use whatever we have in session + } } } - } catch (e2) { - // ignore and continue with stored claims + } catch (refreshErr) { + console.error('token refresh failed', refreshErr && refreshErr.message); + try { req.session.destroy(() => {}); } catch (e2) {} + return res.status(401).send({ code: 401, error: 'Unauthorized' }); } } From f2e7870ea8175dde341338df4e6163172304a11f Mon Sep 17 00:00:00 2001 From: Dan Bruce Date: Fri, 9 Jan 2026 13:37:53 -0500 Subject: [PATCH 2/3] update help --- public/help.html | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/public/help.html b/public/help.html index 69d8148..25d5014 100644 --- a/public/help.html +++ b/public/help.html @@ -66,13 +66,21 @@

Fullsend

Changelog


+

v1.8.4

+

+ Fixed session handling to use refresh-token +

+

v1.8.3

+

+ Changes session checking to use a cookie +

v1.8.2

- Adds a flag for development that shows "DEV MODE" in the navbar. + Adds a flag for development that shows "DEV MODE" in the navbar

v1.8.1

- Fixes how sessions are handled on the user's side. + Fixes how sessions are handled on the user's side

v1.8.0

From bae9c45d6eb1385cabf4c10ff43ed8e46ab487f0 Mon Sep 17 00:00:00 2001 From: Dan Bruce Date: Fri, 9 Jan 2026 13:37:56 -0500 Subject: [PATCH 3/3] 1.8.4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4055e86..9622963 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "fullsend", - "version": "1.8.3", + "version": "1.8.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "fullsend", - "version": "1.8.3", + "version": "1.8.4", "license": "MIT", "dependencies": { "bcryptjs": "^2.4.3", diff --git a/package.json b/package.json index fe6e414..fc94cd6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fullsend", - "version": "1.8.3", + "version": "1.8.4", "description": "Fullsend allows allowed users to send bulk text messages to groups of recipients", "main": "server.js", "scripts": {