fix(sync): rollback to jwt last exp approach
All checks were successful
/ build (map[dockerfile:./services/files/Dockerfile name:files]) (push) Successful in 2m9s
/ build (map[dockerfile:./services/watchers/Dockerfile name:watchers]) (push) Successful in 2m5s
/ build (map[dockerfile:./services/web/Dockerfile name:web]) (push) Successful in 1m59s
/ build (map[dockerfile:./services/app/Dockerfile name:app]) (push) Successful in 1m14s
/ build (map[dockerfile:./services/hasura/Dockerfile name:hasura]) (push) Successful in 1m48s
/ build (map[dockerfile:./services/api/Dockerfile name:api]) (push) Successful in 2m10s
/ build (map[dockerfile:./services/tasks/Dockerfile name:tasks]) (push) Successful in 2m11s
/ deploy (push) Successful in 14s

This commit is contained in:
devthejo 2025-07-01 13:38:18 +02:00
parent 02cb943a93
commit e284f59476
3 changed files with 47 additions and 92 deletions

View file

@ -1,9 +1,10 @@
const { jwtVerify } = require("jose") const { jwtVerify } = require("jose")
const jwtDecode = require("jwt-decode")
const getHasuraClaimsFromJWT = require("@modjo/hasura/utils/jwt/get-hasura-claims-from-jwt") const getHasuraClaimsFromJWT = require("@modjo/hasura/utils/jwt/get-hasura-claims-from-jwt")
const { ctx } = require("@modjo/core") const { ctx } = require("@modjo/core")
const { reqCtx } = require("@modjo/express/ctx") const { reqCtx } = require("@modjo/express/ctx")
module.exports = function (services) { module.exports = function () {
const castIntVars = ["deviceId", "userId"] const castIntVars = ["deviceId", "userId"]
function sessionVarsFromClaims(claims) { function sessionVarsFromClaims(claims) {
const session = { ...claims } const session = { ...claims }
@ -26,7 +27,7 @@ module.exports = function (services) {
} }
return async function auth(jwt, scopes) { return async function auth(jwt, scopes) {
const hasMetaAuthToken = scopes.includes("meta.auth-token") const hasMetaExpUser = scopes.includes("meta.exp-user")
let jwtVerified = false let jwtVerified = false
try { try {
@ -41,32 +42,35 @@ module.exports = function (services) {
} catch (err) { } catch (err) {
const logger = ctx.require("logger") const logger = ctx.require("logger")
// Allow expired JWT only if meta.auth-token scope is present // Allow expired JWT only if meta.exp-user scope is present
if (hasMetaAuthToken && err.code === "ERR_JWT_EXPIRED") { if (hasMetaExpUser && err.code === "ERR_JWT_EXPIRED") {
logger.debug( logger.debug(
{ error: err }, { error: err },
"Allowing expired JWT for meta.auth-token scope" "Allowing expired JWT for meta.exp-user scope"
) )
const req = reqCtx.get("req") // Continue processing with expired JWT
const authTokenJWT = req?.headers?.["x-auth-token"] } else {
if (!authTokenJWT) {
return false
}
const authToken =
services.authTokenHandler.decodeAuthToken(authTokenJWT)
// Create a session that indicates auth token processing is needed
const session = { isAuthTokenRequest: true, authToken }
reqCtx.set("session", session)
return true
}
logger.error({ error: err }, "jwVerify failed") logger.error({ error: err }, "jwVerify failed")
return false return false
} }
}
// Regular user JWT processing
const claims = getHasuraClaimsFromJWT(jwt, claimsNamespace) const claims = getHasuraClaimsFromJWT(jwt, claimsNamespace)
const session = sessionVarsFromClaims(claims) 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)) { if (!isScopeAllowed(session, scopes)) {
return false return false
} }

View file

@ -5,7 +5,7 @@ const { reqCtx } = require("@modjo/express/ctx")
const tasks = require("~/tasks") const tasks = require("~/tasks")
module.exports = function ({ services: { authTokenHandler } }) { module.exports = function () {
const { addTask } = ctx.require("amqp") const { addTask } = ctx.require("amqp")
const redis = ctx.require("redisHotGeodata") const redis = ctx.require("redisHotGeodata")
@ -23,72 +23,41 @@ module.exports = function ({ services: { authTokenHandler } }) {
longitude, longitude,
}, },
} = location } = location
// console.log("addOneGeolocSync", req.body)
const session = reqCtx.get("session") const session = reqCtx.get("session")
let userId
let deviceId
let userBearerJwt = null
// Check if this is an auth token request (set by auth.js) const { deviceId } = session
if (session && session.isAuthTokenRequest) {
// This is an auth token request, process it
try {
logger.debug("Processing auth token for geoloc sync")
const { authToken } = session // Check JWT expiration sequence to prevent replay attacks
const { if (session.exp) {
userId: newUserId, const deviceExpKey = `device:${deviceId}:last_exp`
deviceId: newDeviceId, const storedLastExp = await redis.get(deviceExpKey)
roles,
} = await authTokenHandler.getOrCreateUserSession(
authToken,
req.body.phoneModel,
req.body.deviceUuid
)
userId = newUserId if (storedLastExp && session.exp < parseInt(storedLastExp, 10)) {
deviceId = newDeviceId throw httpError(401, "not the latest jwt")
// Generate new user JWT for token refresh
userBearerJwt = await authTokenHandler.generateUserJwt(
userId,
deviceId,
roles
)
logger.debug({
action: "geoloc-sync-auth-token",
userId,
deviceId,
tokenRefreshed: true,
})
} catch (error) {
logger.error({ error: error.message }, "Failed to process auth token")
if (httpError.isHttpError(error)) {
throw error
}
throw httpError(401, "Invalid auth token")
}
} else if (session && session.userId && session.deviceId) {
// Regular user JWT session
userId = session.userId
deviceId = session.deviceId
logger.debug({ action: "geoloc-sync-user-jwt", userId, deviceId })
} else {
// Invalid session
logger.error({ session }, "Invalid session")
throw httpError(401, "Invalid session")
} }
if (!userId || !deviceId) { // Store the new expiration date
throw httpError(401, "Missing user or device information") if (session.exp !== storedLastExp) {
await redis.set(deviceExpKey, session.exp)
} }
}
const { userId } = session
logger.debug({ action: "geoloc-sync", userId, deviceId })
const coordinates = [longitude, latitude] const coordinates = [longitude, latitude]
await async.parallel([ await async.parallel([
async () => { async () => {
// const transaction = redis.multi()
// transaction.geoadd("device", longitude, latitude, deviceId)
// transaction.publish("deviceSet", deviceId)
// await transaction.exec()
await redis.geoadd("device", longitude, latitude, deviceId) await redis.geoadd("device", longitude, latitude, deviceId)
await addTask(tasks.GEOCODE_MOVE, { deviceId, userId, coordinates }) await addTask(tasks.GEOCODE_MOVE, { deviceId, userId, coordinates })
}, },
async () => async () =>
@ -103,14 +72,7 @@ module.exports = function ({ services: { authTokenHandler } }) {
}), }),
]) ])
const response = { ok: true } return { ok: true }
// Include userBearerJwt in response if token refresh occurred
if (userBearerJwt) {
response.userBearerJwt = userBearerJwt
}
return response
} }
return [addOneGeolocSync] return [addOneGeolocSync]

View file

@ -1,13 +1,6 @@
# description: # description:
x-security: x-security:
- auth: ["user", "meta.auth-token"] - auth: ["user", "meta.exp-user"]
parameters:
- name: X-Auth-Token
in: header
required: false
schema:
type: string
description: Auth token for token refresh when user JWT is expired
requestBody: requestBody:
required: true required: true
content: content:
@ -108,7 +101,3 @@ responses:
properties: properties:
ok: ok:
type: boolean type: boolean
userBearerJwt:
type: string
description: New user JWT token when auth token refresh occurred
nullable: true