From edee8d6bc4309dedc6b179a32011cddce1f9bd72 Mon Sep 17 00:00:00 2001 From: devthejo Date: Sun, 29 Jun 2025 23:29:33 +0200 Subject: [PATCH] fix(sync): allow last expired jwt --- libs/common/oapi/services/auth.js | 44 +++++++++++++++++-- .../src/api/v1/operations/geoloc/sync.post.js | 14 ++++++ .../v1/operations/geoloc/sync.post.spec.yaml | 2 +- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/libs/common/oapi/services/auth.js b/libs/common/oapi/services/auth.js index 50f509d..4765be6 100644 --- a/libs/common/oapi/services/auth.js +++ b/libs/common/oapi/services/auth.js @@ -1,4 +1,5 @@ const { jwtVerify } = require("jose") +const jwtDecode = require("jwt-decode") const getHasuraClaimsFromJWT = require("@modjo/hasura/utils/jwt/get-hasura-claims-from-jwt") const { ctx } = require("@modjo/core") const { reqCtx } = require("@modjo/express/ctx") @@ -18,23 +19,58 @@ module.exports = function () { function isScopeAllowed(session, scopes) { const { allowedRoles } = session - return scopes.some((scope) => allowedRoles.includes(scope)) + return scopes + .filter((scope) => !scope.startsWith("meta.")) + .some((scope) => { + return allowedRoles.includes(scope) + }) } return async function auth(jwt, scopes) { + const hasMetaExpUser = scopes.includes("meta.exp-user") + let jwtVerified = false + try { - if (!jwt || !(await jwtVerify(jwt, JWKSet))) { + if (!jwt) { + return false + } + + jwtVerified = await jwtVerify(jwt, JWKSet) + if (!jwtVerified) { return false } } catch (err) { const logger = ctx.require("logger") - logger.error({ error: err }, "jwVerify failed") - return false + + // Allow expired JWT only if meta.exp-user scope is present + if (hasMetaExpUser && err.code === "ERR_JWT_EXPIRED") { + logger.debug( + { error: err }, + "Allowing expired JWT for meta.exp-user scope" + ) + // Continue processing with expired JWT + } else { + logger.error({ error: err }, "jwVerify failed") + return false + } } const claims = getHasuraClaimsFromJWT(jwt, claimsNamespace) const session = sessionVarsFromClaims(claims) + // Add exp claim to session if meta.exp-user scope is present + if (hasMetaExpUser) { + try { + const payload = jwtDecode(jwt) + if (payload && payload.exp) { + session.exp = payload.exp + } + } catch (err) { + const logger = ctx.require("logger") + logger.error({ error: err }, "Failed to decode JWT for exp claim") + } + } + if (!isScopeAllowed(session, scopes)) { return false } diff --git a/services/api/src/api/v1/operations/geoloc/sync.post.js b/services/api/src/api/v1/operations/geoloc/sync.post.js index b82f701..49e02d4 100644 --- a/services/api/src/api/v1/operations/geoloc/sync.post.js +++ b/services/api/src/api/v1/operations/geoloc/sync.post.js @@ -1,4 +1,5 @@ const async = require("async") +const httpError = require("http-errors") const { ctx } = require("@modjo/core") const { reqCtx } = require("@modjo/express/ctx") @@ -28,6 +29,19 @@ module.exports = function () { const { deviceId } = session + // Check JWT expiration sequence to prevent replay attacks + if (session.exp) { + const deviceExpKey = `device:${deviceId}:last_exp` + const storedLastExp = await redis.get(deviceExpKey) + + if (storedLastExp && session.exp <= parseInt(storedLastExp, 10)) { + throw httpError(401, "not the latest jwt") + } + + // Store the new expiration date + await redis.set(deviceExpKey, session.exp, "EX", 30 * 24 * 60 * 60) // 30 days TTL + } + const { userId } = session logger.debug({ action: "geoloc-sync", userId, deviceId }) diff --git a/services/api/src/api/v1/operations/geoloc/sync.post.spec.yaml b/services/api/src/api/v1/operations/geoloc/sync.post.spec.yaml index a1fca9c..97154a6 100644 --- a/services/api/src/api/v1/operations/geoloc/sync.post.spec.yaml +++ b/services/api/src/api/v1/operations/geoloc/sync.post.spec.yaml @@ -1,6 +1,6 @@ # description: x-security: - - auth: ["user"] + - auth: ["user", "meta.exp-user"] requestBody: required: true content: