No session Cookie received in Redis Cluster setup ... issues on Kubernetes setup

61 Views Asked by At

I am working on an Express.js server that interfaces with a Redis store for session management. In my development environment, the session cookie is being set and sent to the client as expected. However, when I move to the production environment, the session cookie is not being set on the client side. The application is hosted under a subdomain (app.intellioptima.com).

here is my Index.ts file:

/**
 * FILEPATH: c:\Users\AlexT\Desktop\IntelliOptima\backend\usermngmt\src\index.ts
 *
 * This file contains the main entry point for the backend server. It imports and sets up various middleware and routes for the server.
 *
 * @packageDocumentation
 */

import express, { Application, Request, Response } from "express"
import session from 'express-session';
import Redis from "ioredis";

import cors from "cors"
import cookieParser from 'cookie-parser';
import verifyJWT from "./middleware/verifyJWT"
import { verifySession } from "./middleware/verifySession";
import { BusinessRoutes } from "./routes/BusinessRoutes";
import { EmployeeRoutes } from "./routes/EmployeeRoutes";
import { LoginRoutes } from "./routes/LoginRoutes";
import { AppDataSource } from "./data-source"

const isProduction = process.env.NODE_ENV === 'production';

// create express app
const app: Application = express()

// setup express app here

app.use(cors({
    origin: [`http${isProduction ? 's' : ''}://intellioptima-app:81`],
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    credentials: true
}));

if (isProduction) {
    app.set('trust proxy', 1);
}

app.use(express.json());
app.use(cookieParser());


const RedisClusterNodes = [
    { host: 'my-redis-cluster', port: 6379 },
];

const prodRedisOptions = {
    redisOptions: {
        password: process.env.REDIS_PASSWORD // Ensure this environment variable is set
    }
};


const RedisStore = require("connect-redis").default;
const redisClient = new Redis.Cluster(RedisClusterNodes, prodRedisOptions);

redisClient.on("error", function (error) {
    console.error("Redis Error:", error);
});

const store = new RedisStore({
    client: redisClient,
});


app.use(session({
    store: store,
    secret: process.env.SESSION_SECRET!, // Keep using your existing secret
    resave: false,
    saveUninitialized: false,
    cookie: {
        domain: isProduction ? ".intellioptima.com" : ".localhost",
        path: "/",
        secure: isProduction,
        httpOnly: true,
        maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
    },
}));


AppDataSource.initialize().then(async () => {

    // register express routes from defined application routes
    BusinessRoutes.forEach(route => {
        (app as any)[route.method](route.route, verifyJWT, verifySession, (req: Request, res: Response, next: Function) => {
            const result = (new (route.controller as any))[route.action](req, res, next)
            if (result instanceof Promise) {
                result.then(result => result !== null && result !== undefined ? res.send(result) : undefined)

            }
            else if (result !== null && result !== undefined) {
                res.json(result)
            }
        })
    })


    EmployeeRoutes.forEach(route => {
        (app as any)[route.method](route.route, verifyJWT, verifySession, (req: Request, res: Response, next: Function) => {
            const result = (new (route.controller as any))[route.action](req, res, next)
            if (result instanceof Promise) {
                result.then(result => result !== null && result !== undefined ? res.send(result) : undefined)

            } else if (result !== null && result !== undefined) {
                res.json(result)
            }
        })
    })

    LoginRoutes.forEach(route => {
        (app as any)[route.method](route.route, (req: Request, res: Response, next: Function) => {
            const result = (new (route.controller as any))[route.action](req, res, next)
            if (result instanceof Promise) {
                result.then(result => result !== null && result !== undefined ? res.send(result) : undefined)

            } else if (result !== null && result !== undefined) {
                res.json(result)
            }
        })
    })



    app.get('/health', (req, res) => {
        res.status(200).send("Server is running");
    });

    app.get('/api/session', verifySession);


    // start express server
    app.listen(3000)


    console.log("Express server has started on port 3000. Open http://localhost:3000 to see results")

}).catch(error => console.log(error))

This is my cookie handler:

import { Response, Request } from 'express';
import { Login } from '../entity/Login';

export function setJwtCookies(res: Response, token: string, refreshToken: string) {
    const isProduction = process.env.NODE_ENV === 'production';
    res.cookie('token', token, { httpOnly: true, sameSite: 'lax', secure: isProduction, path: "/", domain: isProduction ? '.intellioptima.com' : '.localhost', maxAge: 15 * 60 * 1000 }); // 15 minutes
    res.cookie('refreshToken', refreshToken, { httpOnly: true, sameSite: 'lax', secure: isProduction, path: "/", domain: isProduction ? '.intellioptima.com' : '.localhost', maxAge: 7 * 24 * 60 * 60 * 1000 }); // 7 days
}

export function setRedisSessionCookie(req: Request, user: Login) {
    console.log("setRedisSessionCookie called for user:", user.login_id);
    req.session.user = {
        isAuth: true,
        id: user.login_id
    };
    req.session.save((err) => {
        if(err) {
            console.error("Error saving session:", err);
        }
    });
}

this is my login controller, i'm using typesORM:

export class LoginController {

    private loginRepository = AppDataSource.manager.getRepository(Login);
    private refreshTokenRepository = AppDataSource.manager.getRepository(JwtRefreshToken);

    /**
     * Logs in a user with the given email and password.
     * @param req - The HTTP request object.
     * @param res - The HTTP response object.
     * @param next - The next middleware function.
     * @returns A JSON response indicating whether the login was successful or not.
     */
    async login(req: Request, res: Response, next: NextFunction) {
        console.log("recieved request")
        const { employee_email, login_password } = req.body;

        const JWT_SECRET = process.env.JWT_SECRET;
        const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;

        const user = await this.loginRepository.findOne({
            where: { employee_email },
        });

        if (!user) {
            return res.status(400).json({ error: 'User not found' });
        }

        if (!await bcrypt.compare(login_password, user.login_password)) {
            return res.status(400).json({ error: 'Invalid password' });
        }

        if (!JWT_SECRET) {
            return res.status(500).json({ error: 'JWT Secret is not configured' });
        } else if (!JWT_REFRESH_SECRET) {
            return res.status(500).json({ error: 'JWT Refresh Secret is not configured' });
        }

        const token = jwt.sign({ user_id: user.login_id }, JWT_SECRET, { expiresIn: '15m' });

        // Invalidate all existing refresh tokens for this user
        await this.refreshTokenRepository.update({ login_id: user.login_id }, { revoked: true });

        // Issue a new refresh token
        const refreshTokenValue = jwt.sign({ user_id: user.login_id }, JWT_REFRESH_SECRET, { expiresIn: '7d' });

        const newRefreshToken = this.refreshTokenRepository.create({
            login_id: user.login_id,
            token: refreshTokenValue,
            expire: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days
        });

        await this.refreshTokenRepository.save(newRefreshToken);

        setJwtCookies(res, token, refreshTokenValue);
        setRedisSessionCookie(req, user);
        console.log(res.getHeaders()['set-cookie']);
        
        res.status(200).send({ success: true, message: "User logged in successfully.", user_id: user.login_id });
    }
}

I'm on a kubernetes setup and my Redis cluster is from https://github.com/bitnami/charts/tree/main/bitnami/redis-cluster/#securing-traffic-using-tls

And i Deployed the redis cluster with:

helm install my-redis-cluster bitnami/redis-cluster `
 --namespace intellioptima `
 --set existingSecret=redis-password `
 --set persistence.enabled=false

I tested on local and everything works perfect .. but the moment I go to production version ... I simply won't get the session cookie ... I tried to log out the set-cookie header ... and its like the redisstore isn't resulting in express being able setting the session cookie

I only get the JWT and refreshtoken... and I don't even receive any errors from the redisstore:

req.session.save((err) => {
        if(err) {
            console.error("Error saving session:", err);
        }
    });

and when i manually set the session with save function no errors are being called ... so the session is actually being set ... but somehow it won't be set in the cookie ...

Please help me .. i read the docs .. i tried GPT, and really did my best to figure it out by my self .. but now after a whole month with no result .. I really hope someone could jump on in with me or guide me towards the solution!

Thanks ! :)

I tried a lot of different CORS settings, tested on pgPool instead of redis which gave me the same error, when trying to use postgress sessionStore ...

I tried with stickiness on ingress level... I tried multiple ways of using explicilty RedisCluster-01 i tried so many different approaches and none of them beared any fruits ...

0

There are 0 best solutions below