Reputation: 176
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 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.
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";
});
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
Reputation: 176
So, after some hours...
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!
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