3

so i've been using development servers this entire time, and everything's worked fine. Now, as I've deployed my client and backend, i'm running into an issue where my client cannot grab the sessional data from the cookie. i've checked both the backend and client cookies, and it seems like the session and session.sig are identical, so i don't know what's the deal... here's the relevant code:

my backend: server.js:

dotenv.config({ path: "./.env" });
const cookieKey = process.env.COOKIE_KEY;
const express = require("express");
const cookieSession = require("cookie-session");
const connectDB = require("./config/db");
const passport = require("passport");
const PORT = process.env.PORT || 4500;
const cors = require("cors");

connectDB();

const app = express();
//middleware
app.use(express.json());
app.use(
  cors({
    origin: true, // replace with your frontend domain
    credentials: true,
  })
);

app.use(
  cookieSession({
    maxAge: 24 * 60 * 60 * 1000, // 1 day
    keys: [cookieKey],
    cookie: {
      secure: true,
      sameSite: "none",
    },
  })
);

app.use(passport.initialize());
app.use(passport.session());

const authentication = require("./routes/Authentication.js");
app.use("/api/v1/auth", authentication);

const tabs = require("./routes/Tabs.js"); // Adjust the path as necessary
app.use("/api/v1/tabs", tabs);

const preferences = require("./routes/Preferences.js");
app.use("/api/v1/preferences", preferences);

const google = require("./routes/Google.js"); // Adjust the path as necessary
app.use("/api/v1/google", google);
app.listen(PORT, () => console.log("Server is connected"));

authentication.js:

dotenv.config({ path: "./.env" });
const sucessRedirectURL = process.env.SUCCESS_REDIRECT_URL;
const express = require("express");
const passport = require("passport");
require("../services/Passport");
const router = express.Router();

router.get(
  "/google",
  passport.authenticate("google", {
    scope: ["profile", "email", "https://www.googleapis.com/auth/calendar"],
    accessType: "offline",
    approvalPrompt: "force",
  })
);

router.get(
  "/google/callback",
  passport.authenticate("google", {
    successRedirect: sucessRedirectURL,
  })
);

router.get("/me", (req, res) => {
  if (req.user) {
    res.send(req.user);
  } else {
    res.status(401).json({ message: "Not authenticated" });
  }
});

router.get("/logout", (req, res) => {
  console.log("logging out");
  req.logout();
  res.redirect("/");
});

module.exports = router;

and my own service file, passport.js:

dotenv.config({ path: "./.env" });
const googleClientID = process.env.GOOGLE_CLIENT_ID;
const googleClientSecret = process.env.GOOGLE_CLIENT_SECRET;
const backendAppURL = process.env.BACKEND_APP_URL;

const passport = require("passport");
const GoogleStrategy = require("passport-google-oauth20");
const User = require("../models/User");

//when a user logs in, we get a 'user object' which is serialized to our session by storing a user's ID,
//which is called automatically after logging
passport.serializeUser((user, done) => {
  done(null, user.id);
});
//now, when we want to take the data stored in our session, we use the ID to recreate the full user object on
//each request, which is automatically done on each request
passport.deserializeUser((id, done) => {
  User.findById(id).then((user) => {
    done(null, user);
  });
});
//this code happens first to find/create a user object
passport.use(
  new GoogleStrategy(
    {
      clientID: googleClientID,
      clientSecret: googleClientSecret,
      callbackURL: backendAppURL + "/api/v1/auth/google/callback", //FULL CALLBACK URL IN PRODUCTION VS RELATIVE PATH IN DEVELOPMENT
    },
    async (accessToken, refreshToken, profile, done) => {
      try {
        const existingUser = await User.findOneAndUpdate(
          { googleId: profile.id },
          {
            accessToken,
            refreshToken,
            name: profile.displayName,
            avatarUrl: profile.picture,
            isVerified: profile.emails[0].verified,
          }
        );

        if (existingUser) {
          console.log("Existing user found:", existingUser);

          return done(null, existingUser);
        }

        const user = await new User({
          accessToken,
          refreshToken,
          name: profile.displayName,
          email: profile.emails[0].value,
          googleId: profile.id,
          avatarUrl: profile.picture,
          isVerified: profile.emails[0].verified,
        }).save();
        console.log("New user saved:", user);

        done(null, user);
      } catch (error) {
        console.error("Error during authentication: ", error);
        done(error);
      }
    }
  )
);

here's the backend cookie: enter image description here

sweglord
  • 85
  • 5
  • hey @Philisonstrike, yes they're both on HTTPS domains. i've deployed them using render if that's relevant – sweglord Jul 03 '23 at 06:39
  • please correct as im really new to all this, but i thought in my code, cookie-session creates the session and stores it in req.session, then passport JS uses req.session to populate req.user. my problem is when i try to use the /me endpoint to access the req.user data in my client, it says req.user is null – sweglord Jul 03 '23 at 06:48
  • 1
    oh yeah sure, i'll link the img to the body of my question – sweglord Jul 03 '23 at 07:03
  • hi, sorry for the delay, took a really long time to deploy it successfully, i changed secure and sameSite to be on the same level as maxAge and keys, but secure and samesite are still not being updated in the cookie – sweglord Jul 03 '23 at 07:52
  • sorry what do you mean by a reverse proxy? – sweglord Jul 03 '23 at 08:14
  • ahh, yes i don't handle any SSL stuff, i think render, my hosting service, does all that. how can i check the attriubtes of set-cookie after authenticating? – sweglord Jul 03 '23 at 08:45
  • so sorry for the delay, i had to sleep. here's what my client is getting after authenticating in the set-cookie header: " __cf_bm=0QSLhkB5AwWEkb7zokbT1rzLkoEC7qvtl69iAIEbQXI-1688415261-0-AT0uvwKWj0U3tOCFYrVHdfeWKvLytYrnLfVj7aOVUL8+c/wqWNxqMQ3l3gNh9zXWra8qr3kJc4+cJFSrUP/Axgc=; path=/; expires=Mon, 03-Jul-23 20:44:21 GMT; domain=.onrender.com; HttpOnly; Secure; SameSite=None" – sweglord Jul 03 '23 at 20:16
  • That's looking good. It has both required attributes now. Is that same cookie being sent in the `Cookie` request header for the `/me` route? – Phil Jul 03 '23 at 21:40
  • sorry, i'm a bit confused. what do you mean by that? in my /me route, like the actual endpoint, i can't see anything in the Network panel – sweglord Jul 04 '23 at 00:07
  • I was presuming that after authenticating, you're trying to make a request to `/api/v1/auth/me`. During authentication, you should get a `Set-Cookie` (which you confirmed). When you make requests to the API after that, they _should_ have the cookie present in the `Cookie` request header. You can confirm this by inspecting the request to `/api/v1/auth/me` in your dev-tools _Network_ panel. – Phil Jul 04 '23 at 00:18
  • 1
    oh sorry, some additional info: in the client in the set-cookie header response, there's this warning: "This attempt to set a cookie via Set-Cookie header was blocked because its Domain attribute was invalid in regards to the current host url". – sweglord Jul 04 '23 at 00:18
  • Interesting. You're not setting a `domain` cookie property so it's not clear why it's trying `.onrender.com`. I assume from the error that does not match your actual API domain. – Phil Jul 04 '23 at 00:23
  • no, that's not my domain name, that's the correct top level domain though, my actual api does end in .onrender.com – sweglord Jul 04 '23 at 00:42
  • ahh wait.. do the two domains, my frontend and backend, have to be identical? currently my backend runs on example-web.onrender.com, and my frontend runs on example.onrender.com – sweglord Jul 04 '23 at 00:46
  • No, not at all. If you were able to run them on the same domain however, you wouldn't have had to post this question – Phil Jul 04 '23 at 00:51
  • sorry again, i'm not really sure i understand then. why is it that i cant set cookies ? what does the error mean? – sweglord Jul 04 '23 at 00:54
  • ok, i just tried that, that is these are my new cookie options settings: app.use( cookieSession({ name: "session", maxAge: 24 * 60 * 60 * 1000, // 1 day keys: [cookieKey], secure: true, sameSite: "none", secure: process.env.NODE_ENV === "production", domain: process.env.NODE_ENV === "production" ? "example-web.onrender.com" : "localhost", }) ); where example-web.onrender.com is my actual endpoint URL, but i still get the same error as before – sweglord Jul 04 '23 at 01:16
  • That's looking a bit strange. You've repeated `secure` and should not have a `.` prefix on the domain. Have you confirmed that you're setting the correct `NODE_ENV` when building and deploying for prod? – Phil Jul 04 '23 at 01:19
  • ahh shoot, you're right, i did repeat secure. i redeployed without it and got the same error. and NODE_ENV is set to production on my render variables. i don't know what you mean by the .prefix on my domain. – sweglord Jul 04 '23 at 01:30
  • You edited your comment but originally it had `".example-web.onrender.com"`. Ok, so you've added the `domain` attribute to exactly match your API domain (note this is the **backend** domain, not the front-end), correct? Do you now see that change in the `Set-Cookie` response header? If you're still getting the error, double-check the exact URL used in the request and compare it to the `domain` attribute in `Set-Cookie`. We may be getting to the point where your obfuscating the real domain is hindering debugging – Phil Jul 04 '23 at 01:38
  • the set-cookie's domain property is not changing. i still says: "This attempt to set a cookie via Set-Cookie header was blocked because its Domain attribute was invalid in regards to the current host url". i can confirm the API domain (my backend) is identical to the request. i saw in another stack overflow thread, and have tried both with and without the https:// prefix, to no avail :(. – sweglord Jul 04 '23 at 01:50
  • oh sorryy, i should add, the set-cookie still seems to be showing just .onrender.com for the domain instead of my endpoint's URL: " __cf_bm=7IpoPfqztAGfWoFLH2gCDUaH8eIaV0R4Xof73aLRATk-1688431931-0-ARKs+EfGHsOpOh9brUTQhQ7B6C/23OGYbnNaM0CvAWZDxePPcPMeNzYqHycW759OkZhdRO4zDP57IE8tHPLOk/w=; path=/; expires=Tue, 04-Jul-23 01:22:11 GMT; domain=.onrender.com; HttpOnly; Secure; SameSite=None" – sweglord Jul 04 '23 at 01:51
  • [Render.com: HttpOnly Cookie not being set in browser storage when doing res.cookie between Web Services](https://stackoverflow.com/a/71889837/283366) – Phil Jul 04 '23 at 01:56
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/254358/discussion-between-sweglord-and-phil-is-on-strike). – sweglord Jul 04 '23 at 03:02

1 Answers1

0

Your cookies are not being set with the two attributes required for cross-origin credential sharing; SameSite=None and Secure.

This appears to be due to a mis-configuration of the cookieSession() options...

Other options are passed to cookies.get() and cookies.set() allowing you to control security, domain, path, and signing among other settings.

There's no mention of nesting these "other options" under a cookie key, they simply go into a flat object.

You may also want to tweak these settings based on the build environment so they work locally during development.

const isProd = process.env.NODE_ENV === "production";

app.use(
  cookieSession({
    keys: [cookieKey],
    maxAge: 24 * 60 * 60 * 1000, // 1 day
    secure: isProd,
    sameSite: isProd ? "none" : "lax",
  })
);

Looks like you may need to set the domain cookie property as well to match your production API domain.

Phil
  • 157,677
  • 23
  • 242
  • 245