Search code examples
expressexpress-session

How to handle express-session for each subsequent user?


I am currently working on user authentication and storing JWT tokens in the MongoDB session collection. However, I am encountering an issue with managing multiple sessions. Specifically, I am unable to generate and store sessions for each subsequent user after the first one has successfully logged in.

I am using Thunder Client in VS Code and MongoDB Compass. When I successfully log in with the following credentials:

{
  "email": "mile@email.com",
  "password": "asdfAsdf1!"
}

a session is successfully created and stored in the session collection, and a connect.sid cookie is generated. However, when I attempt to log in with a different set of credentials:

{
  "email": "john@email.com",
  "password": "asdfAsdf2!"
}

and the session ID associated with the credentials for mile@email.com is being regenerated into a new ID, instead of creating an entirely new session and not altering the previously generated one.

Please provide suggestions on how to resolve this issue?

This is my code

app.use(
  session({
    secret: "key_that_will_sign_the_cookie",
    resave: false,
    saveUninitialized: false,
    store: MongoStore.create({
      mongoUrl: "mongodb://127.0.0.1:27017/addressBook",
      collectionName: "sessions",
    }),
    cookie: {
      secure: false,
      maxAge: 15 * 60 * 1000,
    },
  })
); 

userController.ts

export const logIn = asyncHandler(async (req: Request, res: Response) => {
  const { email, password } = req.body;
  const user = await userService.login(email, password);
  const token = tokenService.generateToken(
    user._id,
    "ACCESS_TOKEN_PRIVATE_KEY_HERE"
  );

  // Set session
  (req.session as MySession).jwtToken = token;

  res.status(200).json({
    success: true,
    message: "You have successfully logged in.",
  });
});


export const logout = asyncHandler(async (req: Request, res: Response) => {
  req.session.destroy((err) => {
    if (err) {
      console.log("Error destroying session", err.message);
      res.status(500).json({
        success: false,
        message: "Error logging out.",
      });
    } else {
      res.clearCookie("connect.sid");
      res
        .status(200)
        .json({ success: true, message: "Successfully logged out." });
    }
  });
});

requireAuth.ts (middleware)

const requireAuth = async (req: Request, res: Response, next: NextFunction) => {
  try {
    const { jwtToken } = (req.session as MySession) || {};

    if (!jwtToken) {
      return res.status(401).json({ messagE: messages.unauthorized });
    }

    const { sub: userId } = jwt.verify(
      jwtToken,
      "ACCESS_TOKEN_PRIVATE_KEY_HERE"
    );

    const user = await User.findOne({ _id: userId });

    if (!user) {
      return res.status(401).json({ messagE: messages.unauthorized });
    }

    req.user = user;
    next();
  } catch (error: any) {
    return res.status(401).json({ message: error.message });
  }
};

Update @jfriend00

The idea behind using session is to organize the code and split the logic into multiple files. Service for business logic but it's probably redundant in my case right now (I will refactor the code later). jwt is jsonwebtoken and I am using it to generate authentication token which I am storing in session - see screenshot. Later in requireAuth middleware I am verifying validity of that jwt token and using for protecting all contact routes.

    router.use(requireAuth);

    router
      .get("/", getAllContacts)
      .get("/favorite", getFavoriteContacts)
      .get("/:contactId", getContactById)
      .post("/", addNewContact)
      .patch("/:contactId", updateContact)
      .delete("/:contactId", deleteContact);

tokenService.ts

import jwt from "jsonwebtoken";
import mongoose from "mongoose";

export const generateToken = (
  userId: mongoose.Types.ObjectId,
  secret: string
): string => {
  const payload = {
    sub: userId,
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 15 * 60,
  };

  return jwt.sign(payload, secret);
};

export default { generateToken };

userService.ts

const login = async (email: string, password: string): Promise<IUserDoc> => {
  loginValidation(email, password);

  const user = await getUserByEmail(email);
  const isValidUser = user && (await user.isPasswordMatch(password));

  if (!isValidUser) {
    throw new ApiError(400, messages.incorrectCredentials);
  }

  return user;
};

The session captured in the screenshot is the one stored when I log in with one user (for example, email: mile@email.com). Subsequently, when I enter the credentials for a second user and click 'login,' the expiration date of the session stored for the previous user is updated and new jwtToken is generated also. Why is this happening? Shouldn't a new session be created independently of the first one? Also I want to mention that cookie connect.sid is remaining the same as before, it's not changed.

And yes, instead of having two separate session objects, I still only have one, and after logging in with each subsequent user, only the expiration date is increased and jwtToken is update. (I've also noticed that the timezone of the database is completely wrong, and I need to check this.)

I apologize for any previous confusion.


Solution

  • A session object is connected to a session cookie in a particular browser. If you login as one user and then relogin as a different user in the same browser, it will still use the same session object as the session object is generic and is only associated with the session cookie from the browser. It knows nothing at all about a login. You just use the session object to store your logged in user data. Logging in as a new user, will just overwrite the previous user's data in the session.

    If you have an explicit "logout" function, you could either clear or delete the prior session, but you can't count on that because users aren't required to do that before logging in as someone else. Probably what you should do is when you login a user, you should first reinitialize the session object to be empty (clear all prior data) so there's no risk of contamination from one user to the next. Note, none of this is an issue between logins from different browsers because they won't ever share the same session object.

    Shouldn't a new session be created independently of the first one? Also I want to mention that cookie connect.sid is remaining the same as before, it's not changed.

    A session object from express-session has nothing to do with login. Your login is a whole different layer on top of the session. A session object is connected to a connect.sid cookie. If a client presents the same connect.sid cookie, then your server will fetch the same session object. So, as I explained earlier, if you login as userA in browser 1, that fills in userA's login data into the session object that is associated with the cookie in browser 1. If you then login as a userB in browser 1 before the session cookie expires, it's still got the same connect.sid cookie so it will still be using the exact same session object on the server. Your code, will then put userB's data into that same session object.

    Just remember that express-session is working off a cookie in a specific browser and has nothing at all to do with your own login process. Your login process is just using the underlying session object to store login data.