Compare commits

...

4 Commits

Author SHA1 Message Date
9c4854ce64 🩹 fix: change header name in cookie validation
Some checks failed
Integration Tests / integration-tests (pull_request) Failing after 28s
2026-02-18 12:55:16 +07:00
9e84460a22 ♻️ refactor: create Redis helper and replace direct Redis access 2026-02-18 12:17:26 +07:00
9686153a82 🔒 security: add auth token validation via Redis and DB check 2026-02-17 21:51:14 +07:00
3122f34093 🛂 security: fix auth token validation flow 2026-02-17 21:33:59 +07:00
8 changed files with 92 additions and 11 deletions

View File

@ -9,7 +9,7 @@ export const jwtDecode = (payload: string) => {
try { try {
const decodedPayload = jwt.verify(payload, JWTKey); const decodedPayload = jwt.verify(payload, JWTKey);
return decodedPayload as JWTAuthToken; return decodedPayload as JWTAuthToken;
} catch (error) { } catch {
throw new AppError(401, "Invalid or expired token", error); throw new AppError(403, "Invalid or expired token");
} }
}; };

View File

@ -6,7 +6,7 @@ import { parse } from "cookie";
export const logoutController = async (ctx: Context) => { export const logoutController = async (ctx: Context) => {
try { try {
const jwtToken = parse(ctx.request.headers.get("auth_token") || "") const jwtToken = parse(ctx.request.headers.get("Cookie") || "")
.auth_token as string; .auth_token as string;
const serviceResponse = await logoutService(jwtToken); const serviceResponse = await logoutService(jwtToken);
return returnWriteResponse( return returnWriteResponse(

View File

@ -4,10 +4,10 @@ import { returnReadResponse } from "../../../helpers/callback/httpResponse";
import { mainErrorHandler } from "../../../helpers/error/handler"; import { mainErrorHandler } from "../../../helpers/error/handler";
import { parse } from "cookie"; import { parse } from "cookie";
export const tokenValidationController = (ctx: Context) => { export const tokenValidationController = async (ctx: Context) => {
try { try {
const { auth_token } = parse(ctx.request.headers.get("cookie") || ""); 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( return returnReadResponse(
ctx.set, ctx.set,
200, 200,

View File

@ -1,10 +1,33 @@
import { AppError } from "../../../../helpers/error/instances/app";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { jwtDecode } from "../../../../helpers/http/jwt/decode"; 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 = (payload: string) => { export const tokenValidationService = async (payload: string) => {
try { try {
if (!payload) return null; if (!payload || payload.trim() === "")
throw new AppError(401, "Unauthorized: No token provided");
const decoded = jwtDecode(payload); 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");
await createUserSessionInRedisService({
userId: decoded.user.id,
sessionId: decoded.id,
validUntil: decoded.validUntil,
});
}
return decoded; return decoded;
} catch (error) { } catch (error) {
ErrorForwarder(error); ErrorForwarder(error);

View File

@ -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);
}
};

View File

@ -4,10 +4,11 @@ import { ErrorForwarder } from "../../../helpers/error/instances/forwarder";
import { createUserSessionRepository } from "../repositories/createUserSession.repository"; import { createUserSessionRepository } from "../repositories/createUserSession.repository";
import { redis } from "../../../utils/databases/redis/connection"; import { redis } from "../../../utils/databases/redis/connection";
import { jwtEncode } from "../../../helpers/http/jwt/encode"; import { jwtEncode } from "../../../helpers/http/jwt/encode";
import { createUserSessionInRedisService } from "./internal/createUserSessionInRedis.service";
export const createUserSessionService = async ( export const createUserSessionService = async (
userId: string, userId: string,
userHeaderInfo: UserHeaderInformation userHeaderInfo: UserHeaderInformation,
) => { ) => {
try { try {
// set the date when the token will expire // set the date when the token will expire
@ -29,13 +30,11 @@ export const createUserSessionService = async (
const createUserSession = await createUserSessionRepository(constructData); const createUserSession = await createUserSessionRepository(constructData);
// caching user session data into Redis // caching user session data into Redis
const createRedisKey = `${process.env.APP_NAME}:users:${userId}:sessions:${createUserSession.id}`; await createUserSessionInRedisService({
await redis.hset(createRedisKey, {
userId, userId,
sessionId: createUserSession.id, sessionId: createUserSession.id,
validUntil: createUserSession.validUntil, 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 // create a jwt token with a payload containing the created user session, then return jwt
return jwtEncode(createUserSession); return jwtEncode(createUserSession);

View File

@ -0,0 +1,13 @@
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { checkUserSessionRepository } from "../../repositories/checkUserSession.repository";
export const checkUserSessionInDBService = async (
sessionId: string,
): Promise<boolean> => {
try {
const dbValidationResult = await checkUserSessionRepository(sessionId);
return dbValidationResult ? true : false;
} catch (error) {
ErrorForwarder(error);
}
};

View File

@ -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);
}
};