From 3122f340931f1f32714377ab22ee4560b633b180 Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Tue, 17 Feb 2026 21:33:59 +0700 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=9B=82=20security:=20fix=20auth=20tok?= =?UTF-8?q?en=20validation=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/helpers/http/jwt/decode/index.ts | 4 ++-- src/modules/auth/services/http/tokenValidation.service.ts | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/helpers/http/jwt/decode/index.ts b/src/helpers/http/jwt/decode/index.ts index caed720..36aee54 100644 --- a/src/helpers/http/jwt/decode/index.ts +++ b/src/helpers/http/jwt/decode/index.ts @@ -9,7 +9,7 @@ export const jwtDecode = (payload: string) => { try { const decodedPayload = jwt.verify(payload, JWTKey); return decodedPayload as JWTAuthToken; - } catch (error) { - throw new AppError(401, "Invalid or expired token", error); + } catch { + throw new AppError(403, "Invalid or expired token"); } }; diff --git a/src/modules/auth/services/http/tokenValidation.service.ts b/src/modules/auth/services/http/tokenValidation.service.ts index 602748c..6bf5b4a 100644 --- a/src/modules/auth/services/http/tokenValidation.service.ts +++ b/src/modules/auth/services/http/tokenValidation.service.ts @@ -1,9 +1,11 @@ +import { AppError } from "../../../../helpers/error/instances/app"; import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; import { jwtDecode } from "../../../../helpers/http/jwt/decode"; export const tokenValidationService = (payload: string) => { try { - if (!payload) return null; + if (!payload || payload.trim() === "") + throw new AppError(401, "Unauthorized: No token provided"); const decoded = jwtDecode(payload); return decoded; } catch (error) { From 9686153a829f0077faa650ee2780a657aff1757b Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Tue, 17 Feb 2026 21:51:14 +0700 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=94=92=20security:=20add=20auth=20tok?= =?UTF-8?q?en=20validation=20via=20Redis=20and=20DB=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/tokenValidation.controller.ts | 4 ++-- .../services/http/tokenValidation.service.ts | 24 ++++++++++++++++++- .../checkUserSession.repository.ts | 16 +++++++++++++ .../internal/checkUserSessionInDB.service.ts | 13 ++++++++++ 4 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 src/modules/userSession/repositories/checkUserSession.repository.ts create mode 100644 src/modules/userSession/services/internal/checkUserSessionInDB.service.ts diff --git a/src/modules/auth/controllers/tokenValidation.controller.ts b/src/modules/auth/controllers/tokenValidation.controller.ts index 7e19545..49f641e 100644 --- a/src/modules/auth/controllers/tokenValidation.controller.ts +++ b/src/modules/auth/controllers/tokenValidation.controller.ts @@ -4,10 +4,10 @@ import { returnReadResponse } from "../../../helpers/callback/httpResponse"; import { mainErrorHandler } from "../../../helpers/error/handler"; import { parse } from "cookie"; -export const tokenValidationController = (ctx: Context) => { +export const tokenValidationController = async (ctx: Context) => { try { const { auth_token } = parse(ctx.request.headers.get("cookie") || ""); - const validationResult = tokenValidationService(auth_token as string); + const validationResult = await tokenValidationService(auth_token as string); return returnReadResponse( ctx.set, 200, diff --git a/src/modules/auth/services/http/tokenValidation.service.ts b/src/modules/auth/services/http/tokenValidation.service.ts index 6bf5b4a..aa9b28d 100644 --- a/src/modules/auth/services/http/tokenValidation.service.ts +++ b/src/modules/auth/services/http/tokenValidation.service.ts @@ -1,12 +1,34 @@ import { AppError } from "../../../../helpers/error/instances/app"; import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; import { jwtDecode } from "../../../../helpers/http/jwt/decode"; +import { redis } from "../../../../utils/databases/redis/connection"; +import { checkUserSessionInDBService } from "../../../userSession/services/internal/checkUserSessionInDB.service"; -export const tokenValidationService = (payload: string) => { +export const tokenValidationService = async (payload: string) => { try { if (!payload || payload.trim() === "") throw new AppError(401, "Unauthorized: No token provided"); const decoded = jwtDecode(payload); + + const redisValidationResult = await redis.hgetall( + `${process.env.APP_NAME}:users:${decoded.user.id}:sessions:${decoded.id}`, + ); + if ( + !redisValidationResult || + Object.keys(redisValidationResult).length === 0 + ) { + const dbValidationResult = await checkUserSessionInDBService(decoded.id); + if (!dbValidationResult) + throw new AppError(403, "Unauthorized: Invalid session"); + const createRedisKey = `${process.env.APP_NAME}:users:${decoded.user.id}:sessions:${decoded.id}`; + await redis.hset(createRedisKey, { + userId: decoded.user.id, + sessionId: decoded.id, + validUntil: decoded.validUntil, + }); + await redis.expire(createRedisKey, Number(process.env.SESSION_EXPIRE)); + } + return decoded; } catch (error) { ErrorForwarder(error); diff --git a/src/modules/userSession/repositories/checkUserSession.repository.ts b/src/modules/userSession/repositories/checkUserSession.repository.ts new file mode 100644 index 0000000..9a8bb31 --- /dev/null +++ b/src/modules/userSession/repositories/checkUserSession.repository.ts @@ -0,0 +1,16 @@ +import { AppError } from "../../../helpers/error/instances/app"; +import { userSessionModel } from "../userSession.model"; + +export const checkUserSessionRepository = async (sessionId: string) => { + try { + return await userSessionModel.findUnique({ + where: { + id: sessionId, + isAuthenticated: true, + deletedAt: null, + }, + }); + } catch (error) { + throw new AppError(500, "Database error during session validation", error); + } +}; diff --git a/src/modules/userSession/services/internal/checkUserSessionInDB.service.ts b/src/modules/userSession/services/internal/checkUserSessionInDB.service.ts new file mode 100644 index 0000000..85ab0e3 --- /dev/null +++ b/src/modules/userSession/services/internal/checkUserSessionInDB.service.ts @@ -0,0 +1,13 @@ +import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; +import { checkUserSessionRepository } from "../../repositories/checkUserSession.repository"; + +export const checkUserSessionInDBService = async ( + sessionId: string, +): Promise => { + try { + const dbValidationResult = await checkUserSessionRepository(sessionId); + return dbValidationResult ? true : false; + } catch (error) { + ErrorForwarder(error); + } +}; From 9e84460a22e9b36cb5c8d15f4438bc0aa94391ae Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Wed, 18 Feb 2026 12:17:26 +0700 Subject: [PATCH 3/5] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20create=20R?= =?UTF-8?q?edis=20helper=20and=20replace=20direct=20Redis=20access?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/http/tokenValidation.service.ts | 5 ++-- .../services/createUserSession.service.ts | 7 ++--- .../createUserSessionInRedis.service.ts | 30 +++++++++++++++++++ 3 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 src/modules/userSession/services/internal/createUserSessionInRedis.service.ts diff --git a/src/modules/auth/services/http/tokenValidation.service.ts b/src/modules/auth/services/http/tokenValidation.service.ts index aa9b28d..f078b91 100644 --- a/src/modules/auth/services/http/tokenValidation.service.ts +++ b/src/modules/auth/services/http/tokenValidation.service.ts @@ -3,6 +3,7 @@ import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; import { jwtDecode } from "../../../../helpers/http/jwt/decode"; import { redis } from "../../../../utils/databases/redis/connection"; import { checkUserSessionInDBService } from "../../../userSession/services/internal/checkUserSessionInDB.service"; +import { createUserSessionInRedisService } from "../../../userSession/services/internal/createUserSessionInRedis.service"; export const tokenValidationService = async (payload: string) => { try { @@ -20,13 +21,11 @@ export const tokenValidationService = async (payload: string) => { const dbValidationResult = await checkUserSessionInDBService(decoded.id); if (!dbValidationResult) throw new AppError(403, "Unauthorized: Invalid session"); - const createRedisKey = `${process.env.APP_NAME}:users:${decoded.user.id}:sessions:${decoded.id}`; - await redis.hset(createRedisKey, { + await createUserSessionInRedisService({ userId: decoded.user.id, sessionId: decoded.id, validUntil: decoded.validUntil, }); - await redis.expire(createRedisKey, Number(process.env.SESSION_EXPIRE)); } return decoded; diff --git a/src/modules/userSession/services/createUserSession.service.ts b/src/modules/userSession/services/createUserSession.service.ts index cefb850..ac275c3 100644 --- a/src/modules/userSession/services/createUserSession.service.ts +++ b/src/modules/userSession/services/createUserSession.service.ts @@ -4,10 +4,11 @@ import { ErrorForwarder } from "../../../helpers/error/instances/forwarder"; import { createUserSessionRepository } from "../repositories/createUserSession.repository"; import { redis } from "../../../utils/databases/redis/connection"; import { jwtEncode } from "../../../helpers/http/jwt/encode"; +import { createUserSessionInRedisService } from "./internal/createUserSessionInRedis.service"; export const createUserSessionService = async ( userId: string, - userHeaderInfo: UserHeaderInformation + userHeaderInfo: UserHeaderInformation, ) => { try { // set the date when the token will expire @@ -29,13 +30,11 @@ export const createUserSessionService = async ( const createUserSession = await createUserSessionRepository(constructData); // caching user session data into Redis - const createRedisKey = `${process.env.APP_NAME}:users:${userId}:sessions:${createUserSession.id}`; - await redis.hset(createRedisKey, { + await createUserSessionInRedisService({ userId, sessionId: createUserSession.id, validUntil: createUserSession.validUntil, }); - await redis.expire(createRedisKey, Number(process.env.SESSION_EXPIRE)); // create a jwt token with a payload containing the created user session, then return jwt return jwtEncode(createUserSession); diff --git a/src/modules/userSession/services/internal/createUserSessionInRedis.service.ts b/src/modules/userSession/services/internal/createUserSessionInRedis.service.ts new file mode 100644 index 0000000..77cf6d8 --- /dev/null +++ b/src/modules/userSession/services/internal/createUserSessionInRedis.service.ts @@ -0,0 +1,30 @@ +import { AppError } from "../../../../helpers/error/instances/app"; +import { redis } from "../../../../utils/databases/redis/connection"; + +export const createUserSessionInRedisService = async ({ + userId, + sessionId, + validUntil, + exp, +}: { + userId: string; + sessionId: string; + validUntil?: Date; + exp?: number; +}) => { + try { + const createRedisKey = `${process.env.APP_NAME}:users:${userId}:sessions:${sessionId}`; + await redis.hset(createRedisKey, { + userId, + sessionId, + validUntil: validUntil, + }); + await redis.expire( + createRedisKey, + exp || Number(process.env.SESSION_CACHE_EXPIRE!), + ); + return true; + } catch (error) { + throw new AppError(500, "Error creating user session in Redis", error); + } +}; From 9c4854ce64fd4e9fe79004398944bf9a0d146b78 Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Wed, 18 Feb 2026 12:55:16 +0700 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=A9=B9=20fix:=20change=20header=20nam?= =?UTF-8?q?e=20in=20cookie=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/auth/controllers/logout.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/auth/controllers/logout.controller.ts b/src/modules/auth/controllers/logout.controller.ts index 75a42ef..083b802 100644 --- a/src/modules/auth/controllers/logout.controller.ts +++ b/src/modules/auth/controllers/logout.controller.ts @@ -6,7 +6,7 @@ import { parse } from "cookie"; export const logoutController = async (ctx: Context) => { try { - const jwtToken = parse(ctx.request.headers.get("auth_token") || "") + const jwtToken = parse(ctx.request.headers.get("Cookie") || "") .auth_token as string; const serviceResponse = await logoutService(jwtToken); return returnWriteResponse( From 711ca4519cfd17a8c36839f8aeb8b36558ee3e8c Mon Sep 17 00:00:00 2001 From: Rafi Arrafif Date: Wed, 18 Feb 2026 13:00:50 +0700 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=9A=A8=20fix:=20resolve=20linting=20e?= =?UTF-8?q?rror?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/userSession/services/createUserSession.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/modules/userSession/services/createUserSession.service.ts b/src/modules/userSession/services/createUserSession.service.ts index ac275c3..a59cc6d 100644 --- a/src/modules/userSession/services/createUserSession.service.ts +++ b/src/modules/userSession/services/createUserSession.service.ts @@ -2,7 +2,6 @@ import { Prisma } from "@prisma/client"; import { UserHeaderInformation } from "../../../helpers/http/userHeader/getUserHeaderInformation/types"; import { ErrorForwarder } from "../../../helpers/error/instances/forwarder"; import { createUserSessionRepository } from "../repositories/createUserSession.repository"; -import { redis } from "../../../utils/databases/redis/connection"; import { jwtEncode } from "../../../helpers/http/jwt/encode"; import { createUserSessionInRedisService } from "./internal/createUserSessionInRedis.service";