astroxii
astroxii

Reputation: 176

express-session for cross-domain application not sending cookies

Setup

Backend: Express.js, express-session, connect-mongo, cors, modules working in Node.js host from Heroku (Free edition), and storing data to MongoDB Atlas (cloud-based Mongo solution)

Frontend: React.js, axios, working in Godaddy shared linux host.

I use the Heroku free domain (https://app-name.herokuapp.com), and a bought domain for Godaddy.

Also, for free HTTPS Certificate, i use Cloudflare (Free edition).

The problem

The communication between frontend and backend works well. Data is sent and is received. But, in the login system i need to implement, the data is sent, passes the database check correctly, and after it, i save the session to the database. It works well, until THE COOKIE SETTING PART, WHEN I DON'T RECEIVE THE SESSION COOKIE IN THE FRONTEND DOMAIN.

Code

app.js

// In my app.js, the entry point for backend
// ---> This line is just after module importing.

dotenv.config();

const storage = new store({
   mongoUrl: ""+process.env.MONGODB_URI, collectionName: "sessions", ttl: 100, autoRemove: "native"
});

app.disable("X-Powered-By");

app.use(cors({origin: "https://my.godaddy.subdomain", credentials: true, methods: "GET, POST, PUT, DELETE"}));
app.use(function(req, res, next) {
   res.header("Access-Control-Allow-Credentials", true);
   res.header("Access-Control-Allow-Origin", "https://my.godaddy.subdomain");
   res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-HTTP-Method-Override");
   res.header("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT,OPTIONS");
   next();
});

// "my.godaddy.subdomain" is the SUBDOMAIN where i have an Administrators' login page, so it is separated from public site "godaddy.subdomain" (ONLY FOR EXAMPLE PURPOSE)

app.use(express.json());
app.use(express.urlencoded({extended: false}));

mongo.connect(""+process.env.MONGODB_URI,
{
   useNewUrlParser: true,
   useUnifiedTopology: true,
   dbName: "Test_DB"
}
).then(() => console.log("Connected to MongoDB!"))
.catch((err) => console.log("Error connecting to MongoDB: "+err));

app.use(session({
   name: "admin_session",
   secret: ""+process.env.SERVER_SECRET,
   resave: true,
   rolling: false,
   saveUninitialized: false,
   unset: "destroy",
   cookie:  {
      sameSite: "none",  // I tried changing this a thousand times, no result...
      secure: true,      // The maximum i could was set a session sucessfully IN THE HEROKU
      httpOnly: true,    // BACKEND (no cross-domain), but that would be useless...
      maxAge: 8600000
   },
   store: storage
}));

// ---> After this line, the routing code, all working fine.

admin_routing.js

// The Admin route, called admin_routing.js, used in app.js

router.route("/admin/login").post(async (req, res, next) =>
{
        const {username, password} = req.body;
        const admin = await Admin.find({user_login: username}).then((doc) => doc.pop());

        if(admin.user_login === username)
        {
            await bcrypt.compare(password, admin.user_pass).then((same) =>
            {
                if(same)
                {
                    req.session.adminID = admin._id;
                    req.session.save((err) => console.log(err)); // here i save the session
                    console.log(req.body, req.session);          // it is sucessfully saved but
                    res.send({error: "Sucess!"});               // NO COOKIE IS SENT...
                }
                else
                {
                    res.send({error: "Check your credentials and try again."});
                }
            });
        }
        else
        {
            res.send({error: "No administrators found."});
        }
});

Frontend request (Login component)

// The part of my frontend where i post login data, in a React.js Component

await axios.post("https://my-app.herokuapp.com/admin/login", 
    {username: Inputs[0].value, password: Inputs[1].value}, {withCredentials: true, method: "POST", 
    headers: {'Access-Control-Allow-Origin': 'https://my-app.herokuapp.com'}})
    .then((res) =>
    {
      document.getElementById("err-bc").innerText = ""+res.data.error; // Design purpose
      document.getElementById("err-bc").style.visibility = "visible";
      console.log(res.data);
    }).catch((err) =>
    {
      document.getElementById("err-bc").innerText = ""+res.data.error; // Design purpose
      document.getElementById("err-bc").style.visibility = "visible";
    });

How it should work when fixed

It must save the session, send a cookie back to my frontend in my subdomain, and when the logged in ADMIN user goes to public pages it must still keep the cookie for authentication. So adm.mydomain.com receives the authentication cookie, and keeps it when the user goes to mydomain.com AND www.mydomain.com (public pages).

Hope someone have the answer, now i've been trying to solve for two days.

Thanks.

Upvotes: 4

Views: 4359

Answers (1)

astroxii
astroxii

Reputation: 176

So, after some hours...

Solution for own question

After some tests using non-express-session cookies, res.cookie() native from Express, and enabling "trust proxy", i solved the problem and it works correctly now!

The changes:

app.js

// In my app.js, the entry point for backend
// ---> This line is just after module importing.

dotenv.config();

const storage = new store({
   mongoUrl: ""+process.env.MONGODB_URI, collectionName: "sessions", ttl: 100, autoRemove: "native"
});

app.disable("X-Powered-By");

app.set("trust proxy", 1); // -------------- FIRST CHANGE ----------------

app.use(cors({origin: "https://my.godaddy.subdomain", credentials: true, methods: "GET, POST, PUT, DELETE"}));
app.use(function(req, res, next) {
   res.header("Access-Control-Allow-Credentials", true);
   res.header("Access-Control-Allow-Origin", "https://my.godaddy.subdomain");
   res.header("Access-Control-Allow-Headers",
   "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-HTTP-Method-Override, Set-Cookie, Cookie");
   res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
   next();  
});    // --------------- SECOND CHANGE -------------------

// "my.godaddy.subdomain" is the SUBDOMAIN where i have an Administrators' login page, so it is separated from public site "godaddy.subdomain" (ONLY FOR EXAMPLE PURPOSE)

app.use(express.json());
app.use(express.urlencoded({extended: false}));

mongo.connect(""+process.env.MONGODB_URI,
{
   useNewUrlParser: true,
   useUnifiedTopology: true,
   dbName: "Test_DB"
}
).then(() => console.log("Connected to MongoDB!"))
.catch((err) => console.log("Error connecting to MongoDB: "+err));

app.use(session({
   name: "admin_session",
   secret: ""+process.env.SERVER_SECRET,
   resave: true,
   rolling: false,
   saveUninitialized: false,
   unset: "destroy",
   cookie:  {
      sameSite: "none",
      secure: true,      
      httpOnly: true, 
      maxAge: 8600000
   },
   store: storage
}));

// ---> After this line, the routing code, all working fine.

admin_routing.js

// The Admin route, called admin_routing.js, used in app.js

router.route("/admin/login").post(async (req, res, next) =>
{
        const {username, password} = req.body;
        const admin = await Admin.find({user_login: username}).then((doc) => doc.pop());

        if(admin.user_login === username)
        {
            await bcrypt.compare(password, admin.user_pass).then((same) =>
            {
                if(same)
                {
                    req.session.adminID = admin._id;
                    req.session.save((err) => console.log(err));
                    res.header("Content-Type", "application/json"); // ------- THIRD CHANGE --------      
                    res.send({error: "Sucess!"});               
                }
                else
                {
                    res.send({error: "Check your credentials and try again."});
                }
            });
        }
        else
        {
            res.send({error: "No administrators found."});
        }
});

After the changes...

So now the cookies are sent once i log in, but a new problem appears: COOKIES WON'T PERSIST IN THE BROWSER. But luckily, this was easy to solve: since i get a cookie with my API/Backend domain, it surely won't keep in my browser when i reload, so, to solve this simply added a GET request wich saves the session again (req.session.save();), called every browser refresh or site opening, and it gets the session data when needed. After all, the session persists, and is recognized when the cookie is sent in every GET request the Frontend makes.

Code

admin_router.js

router.route("/auth/check").get(async(req, res, next) => 
// THIS route is called evertime the window refreshs (what is a bit rare in React.js) OR when data is needed (working...)
{
    if(req.session.adminID)
    {
        const admin = await Admin.find({_id: ""+req.session.adminID}).then((doc) => doc.pop());
        req.session.save(); //IN THIS PART, cookie is sent back to Frontend again, making the session persist correctly
        res.send({hasSession: true, admin: {name: admin.name, picture: admin.user_picture}}); // THIS data is saved to localStorage, since is public data, so i don't need to get it everytime
    }
    else
    {
        res.send({msg: "No session started.", hasSession: false}); 
    }
});

And that is all. Hope this helps anyone with the same issue. Good luck and if this is still a problem for you, feel free to post it here.

Thanks. :)

Upvotes: 4

Related Questions