Express.js httpOnly cookie not being set - javascript

I've set up an API with a create user and an auth route. The auth route should set an httpOnly cookie containing a JWT, and should send JSON for the client to store in localhost.
In the front-end I'm doing a simple fetch.
The server responds 200 and with the JSON I expect, but somehow, the cookie doesn't get set.
However, in Postman, the cookie does indeed get set.
Express server
const express = require('express')
const cors = require('cors')
// boilerplate stuff
app.use(express.json())
app.use(cors({ origin: 'http://localhost:3000', credentials: true }))
app.post('auth', (req, res) => {
// fetch user from db, validation, bla bla bla
const token = jwt.sign({ issuer: user.id }, keys.private, { algorithm: 'RS256' })
res.cookie('token', token, { httpOnly: true })
res.json(user)
})
Next.js front-end
const handleSubmit = async (e) => {
e.preventDefault()
try {
const res = await fetch('http://localhost:5000/api/v1/auth', {
method: 'post',
mode: 'cors',
credentials: 'include',
headers: {
'content-type': 'application/json',
'accept': 'application/json',
},
body: JSON.stringify(formState),
})
const data = await res.json()
console.log(data)
} catch (err) {
console.error(err)
setError(err.message)
}
}

'Twas resolved.
I was looking in Session Storage as opposed to Cookies in my devtools.

Related

HttpOnly cookies are not sent with request to server

I've created API in node express and I'm running it on port :8000, I am consuming APIs through simple CRA on port :3000. I've created registration and login with setting httpOnly cookie. Furthermore, I've put middleware to check each endpoint in order to verify if it has that token.
When I test through Thunder/Postman everything works, after logging in I get the cookie in response, I set that cookie as auth token and make request to get data and I get the data.
When I log in through the React Frontend it succeeds and I can see in network tab that I have received the cookie in response. But when I make a request to protected endpoint, the request does not have a cookie in it (I log incoming requests on server and compare ones made with Thunder/Postman client and via app in Browser).
I use axios, and I've put {withCredentials: true} it doesn't work. I've used withAxios hook and it doesn't work either.
SERVER
index.js
...
const app = express()
app.use(cors({
credentials: true,
origin: 'http://localhost:3000',
}));
...
controllers/User.js
...
const loginUser = async(req, res) => {
const body = req.body
const user = await User.findOne({ email: body.email })
if(user) {
const token = generateToken(user)
const userObject = {
userId: user._id,
userEmail: user.email,
userRole: user.role
}
const validPassword = await bcrypt.compare(body.password, user.password)
if(validPassword) {
res.set('Access-Control-Allow-Origin', req.headers.origin);
res.set('Access-Control-Allow-Credentials', 'true');
res.set(
'Access-Control-Expose-Headers',
'date, etag, access-control-allow-origin, access-control-allow-credentials'
)
res.cookie('auth-token', token, {
httpOnly: true,
sameSite: 'strict'
})
res.status(200).json(userObject)
} else {
res.status(400).json({ error: "Invalid password" })
}
} else {
res.status(401).json({ error: "User doesn't exist" })
}
}
...
middleware.js
...
exports.verify = (req, res, next) => {
const token = req.headers.authorization
if(!token) res.status(403).json({ error: "please provide a token" })
else {
jwt.verify(token.split(" ")[1], tokenSecret, (err, value) => {
if(err) res.status(500).json({error: "failed to authenticate token"})
req.user = value.data
next()
})
}
}
...
router.js
...
router.get('/bills', middleware.verify, getBills)
router.post('/login', loginUser)
...
CLIENT
src/components/LoginComponent.js
...
const loginUser = (e) => {
setLoading(true)
e.preventDefault()
let payload = {email: email, password: password}
axios.post('http://localhost:8000/login', payload).then(res => res.status === 200
? (setLoading(false), navigate('/listbills')) : navigate('/register'))
}
...
src/components/ListBills.js
...
useEffect(() => {
fetch('http://localhost:8000/bills', {
method: 'get',
headers: {'Content-Type': 'application/json'},
credentials: 'include',
})
.then(response => {console.log(response)}).catch(err => console.log(err));
}, [])
...
I've also tried:
axios.get('http://localhost:8000/bills',{withCredentials: true})
.then((data) => console.log(data))
.then((result) => console.log(result))
.catch((err) => console.log('[Control Error ] ', err))
}
and
const [{ data, loading, error }, refetch] = useAxios(
'http://localhost:8000/bills',{
withCredentials: true,
headers: {'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json'
}})
Console.log error:
After I login I get this in Network tab:
However when I want to access the list:
=== UPDATE ===
So the cause of the issue is not having the httpOnly cookie passed in the request header. This is the log of the middleware I am using:
token undefined
req headers auth undefined
req headers {
host: 'localhost:8000',
connection: 'keep-alive',
'sec-ch-ua': '" Not;A Brand";v="99", "Google Chrome";v="97", "Chromium";v="97"',
'sec-ch-ua-mobile': '?0',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36',
'sec-ch-ua-platform': '"macOS"',
'content-type': 'application/json',
accept: '*/*',
origin: 'http://localhost:3000',
'sec-fetch-site': 'same-site',
'sec-fetch-mode': 'cors',
'sec-fetch-dest': 'empty',
referer: 'http://localhost:3000/',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'en-US,en;q=0.9,hr;q=0.8,sr;q=0.7,bs;q=0.6,de;q=0.5,fr;q=0.4,it;q=0.3'
}
token is read from headers.authorization but from the log of the headers it doesn't exist so my requests fail to be authorized.
Still not working.
After reading everything on CORS and httpOnly cookies I've managed to get it working.
First I removed sameSite and added domain prop according to documentation in controllers/User.js on SERVER
res.cookie('auth-token', token, {
httpOnly: true,
domain: 'http://localhost:3000'
})
Then I got a little yellow triangle in the console request view, it said that domain was invalid. Then I just changed domain to origin and the cookie appeared in the request log of the headers 🎉
res.cookie('auth-token', token, {
httpOnly: true,
origin: 'http://localhost:3000',
})
The cookie was not in the Authorization property of the headers but in the cookie so I had to change the code in the middleware.js since it expected format bearer xxyyzz but receiving auth-token=xxyyzz, it looks like this now:
exports.verify = (req, res, next) => {
const token = req.headers.cookie
if(!token) res.status(403).json({ error: "please provide a token" })
else {
jwt.verify(token.split("=")[1], tokenSecret, (err, value) => {
if(err) res.status(500).json({error: "failed to authenticate token"})
req.user = value.data
next()
})
}
}

MERN stack herokuapp does not work in edge and firefox browser. CORS preflight error

I have build a React Redux app. It works fine in localhost in every browser(chrome,edge,firefox). But the Heroku deployed app doesn't works in Edge, Firefox , although it worked in chrome perfectly.
My app doesn't seems to send request and receive response ,because i noticed that the login button spinner keeps on spinning because it waits for response. So i think that no response is received from backend.
Following are the conclusion i made after test in different browser:
it works perfectly in every browser in localhost development mode.
After Deploying , herokuapp works perfectly fine in chrome only.
Herokuapp doesn't work in edge , Firefox.
Same is the issue with other system(Friends PC).
It doesn't work in any browser except chrome in Mobile device
After console logging and banging my head for hours i got the following error in microsoft edge:
Access to XMLHttpRequest at 'https://ecrypt.herokuapp.com/user/login' from origin 'http://ecrypt.herokuapp.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header has a value 'https://ecrypt.herokuapp.com' that is not equal to the supplied origin.
2.2c59d01c.chunk.js:2 POST https://ecrypt.herokuapp.com/user/login net::ERR_FAILED
Okay, so i figured out that there is some problem with CORS.
Following is my code
Frontend:
import axios from "axios";
const API = axios.create({
baseURL: "https://ecrypt.herokuapp.com",
// withCredentials: false,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,PUT,POST,DELETE,PATCH,OPTIONS",
"Access-Control-Allow-Headers": "Origin, Content-Type, X-Auth-Token",
},
});
// const API = axios.create({ baseURL: "http://localhost:9000" });
const cloudinaryAPI = axios.create({
baseURL: "https://api.cloudinary.com/v1_1/ecryptimgdb",
});
//register new user
export const registerNewUser = (formData) =>
API.post("/user/register", formData);
//Account Activation through Email
export const activation = (activation_token) =>
API.post("/user/activation", {
data: {
activation_token,
},
});
//Login
export const login = (formData) =>
API.post("/user/login", formData, { withCredentials: true });
//get Token
export const getToken = () =>
API.post("/user/refresh_token", null, { withCredentials: true });
//Logout
export const logout = () => API.get("/user/logout", { withCredentials: true });
//get User
export const getUser = (token) =>
API.get("/user/info", {
headers: { Authorization: `${token}` },
});
//PROFILE SETTINGS__________________________________________________________________________________________
export const editProfile = (token, profileData) =>
API.post(
"/user/updateProfile",
{ profileData },
{
headers: { Authorization: `${token}` },
}
);
//forgot password____
export const forgotPass = (email) =>
API.post("/user/forgotPassword", { email });
//reset password_____
export const resetPass = (token, password) =>
API.post(
"/user/resetPassword",
{ password },
{
headers: { Authorization: `${token}` },
}
);
//change password____
export const changePass = (oldPassword, newPassword, token) =>
API.post(
"/user/changePassword",
{ oldPassword, newPassword },
{
headers: { Authorization: `${token}` },
}
);
BACKEND:
//IMPORTS
require("dotenv").config();
const express = require("express");
const bodyParser = require("body-parser");
const mongoose = require("mongoose");
const cors = require("cors");
const cookiesParser = require("cookie-parser");
const path = require("path");
const app = express();
// app.use(cors({ credentials: true, origin: "http://localhost:3000" }));
app.use(cors({ credentials: true, origin: "https://ecrypt.herokuapp.com" }));
app.use(cookiesParser());
// app.use(bodyParser.json({ limit: "30mb", extended: true }));
// app.use(bodyParser.urlencoded({ limit: "30mb", extended: true }));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const CONNECTION_URL = process.env.MONGODB_URL;
// const CONNECTION_URL = process.env.MONGODB_LOCAL_URL;
const PORT = process.env.PORT || 9000;
//MONGODB CLOUD DATABASE CONNECTION________________________
mongoose
.connect(CONNECTION_URL, {
useCreateIndex: true,
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false,
})
.then(() => console.log("Connected to Database :: MongoDB Cloud"))
.catch((err) => console.log(err.message));
// app.use("/", routesIndex);
app.use("/", require("./routes/index"));
if (process.env.NODE_ENV === "production") {
app.use(express.static("client/build"));
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "client", "build", "index.html"));
});
}
//SERVER LISTENING
app.listen(PORT, (err) => {
if (err) {
console.log(err.message);
} else {
console.log(`Listening on localhost:${PORT}`);
}
});
NOTE
I am using cookies to store token so i needed withCredentials and Authorization headers.
The following headers should be sent by the server (instead of the front)
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET,PUT,POST,DELETE,PATCH,OPTIONS",
"Access-Control-Allow-Headers": "Origin, Content-Type, X-Auth-Token",
},
Try removing headers from your axios request. I don't think those headers are allowed.
Ok, so i figured it out ,in my case i was setting headers as
{
headers: { Authorization: `${token}` },
}
Instead of setting it like above a slight change just worked for me:
{
headers: { Authorization: `Bearer ${token}` },
}
and at backend side in index.js or server.js whatever your file name is ,use cors middle ware like this:
const cors = require("cors");
app.use(
cors({
origin: ["https://blahblah.herokuapp.com", "http://localhost:****"],
credentials: true,
})
);
Note: credentials true if you want to pass cookies and access them at server side.
In my case i wanted to access the HttpOnly cookies at server side.

React, axios, content-type

I have already searched a lot, but none of the solutions found work: Cannot send content-type by axios. but if I use the postman interceptor and I 'send' the request generated by axios this time it works: the node.js / express server correctly receives the request and body-parser works normally!
React side:
const API_URL = "http://localhost:8800/auth/";
const headers = {
accept: 'application/json, text/plain, */*',
'content-type': 'application/json;charset=UTF-8'
};
class AuthService {
register(pseudo, email, password) {
return axios.post(API_URL + "signup/",
{ pseudo, email, password },
{ headers: headers})
.then(response => {
if (response.data.accessToken) {
localStorage.setItem("user", JSON.stringify(response.data));
}
return response.data;
});
}
server side
const app = express();
app.use(function (req, res, next) {
console.log( req.headers);
next();
});
app.use( bodyParser.urlencoded({ extended: true }), bodyParser.json());
Usually when I use axios I send the headers in a config variable like this and I stringify the body so it sends as JSON object and not a JS object.
const config = {
headers: {
'Content-Type': 'application/json',
},
};
const body = JSON.stringify({arguments});
try {
const res = await axios.post(/url, body, config);
...
Here's a link to the docs for a little more reading about it:
https://github.com/axios/axios

Unable to access cookie from fetch() response

Even though this question is asked several times at SO like:
fetch: Getting cookies from fetch response
or
Unable to set cookie in browser using request and express modules in NodeJS
None of this solutions could help me getting the cookie from a fetch() response
My setup looks like this:
Client
export async function registerNewUser(payload) {
return fetch('https://localhost:8080/register',
{
method: 'POST',
body: JSON.stringify(payload),
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
});
}
...
function handleSubmit(e) {
e.preventDefault();
registerNewUser({...values, avatarColor: generateAvatarColor()}).then(response => {
console.log(response.headers.get('Set-Cookie')); // null
console.log(response.headers.get('cookie')); //null
console.log(document.cookie); // empty string
console.log(response.headers); // empty headers obj
console.log(response); // response obj
}).then(() => setValues(initialState))
}
server
private setUpMiddleware() {
this.app.use(cookieParser());
this.app.use(bodyParser.urlencoded({extended: true}));
this.app.use(bodyParser.json());
this.app.use(cors({
credentials: true,
origin: 'http://localhost:4200',
optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204
credentials: true
}));
this.app.use(express.static(joinDir('../web/build')));
}
...
this.app.post('/register', (request, response) => {
const { firstName, lastName, avatarColor, email, password }: User = request.body;
this.mongoDBClient.addUser({ firstName, lastName, avatarColor, email, password } as User)
.then(() => {
const token = CredentialHelper.JWTSign({email}, `${email}-${new Date()}`);
response.cookie('token', token, {httpOnly: true}).sendStatus(200); // tried also without httpOnly
})
.catch(() => response.status(400).send("User already registered."))
})
JavaScript fetch method won't send client side cookies and silently ignores the cookies sent from Server side Reference link in MDN, so you may use XMLHttpRequest method to send the request from your client side.
I figured it out. The solution was to set credentials to 'include' like so:
export async function registerNewUser(payload) {
return fetch('https://localhost:8080/register',
{
method: 'POST',
body: JSON.stringify(payload),
credentials: 'include',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
});
}
After that I needed to enabled credentials in my cors middleware:
this.app.use(cors({
credentials: true, // important part here
origin: 'http://localhost:4200',
optionsSuccessStatus: 200
})
And then finally I needed to remove the option {httpOnly: true} in the express route response:
response.cookie('token', '12345ssdfsd').sendStatus(200);
Keep in mind if you send the cookie like this, it is set directly to the clients cookies. You can now see that the cookie is set with: console.log(document.cookie).
But in a practical environment you don't want to send a cookie that is accessible by the client. You should usually use the {httpOnly: true} option.

Fetch http POST request sending EMPTY object to Node server

I'm trying to make an HTTP Post request to my Node server from an html form. I use the Body Parser and everything but when I try to fetch the request, and log the req.body in my server, it's an EMPTY object. Do you know how I can fix this?
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
const form = document.querySelector('form');
form.addEventListener('submit', e => {
e.preventDefault();
const email = document.querySelector('#email').value;
const password = document.querySelector('#password').value;
const formData = { email, password };
const options = {
headers: {
'Content-Type': 'application/json'
},
method: 'POST',
body: JSON.stringify(formData),
mode: 'no-cors'
};
fetch('http://localhost:3000/users/login', options)
.then(res => {
console.log(res);
res.json();
})
.then(data => {
console.log(data);
});
You said mode: 'no-cors' so everything that requires CORS permission is ignored (including setting the Content-Type header to application/json).
Remove mode: 'no-cors' (and use the cors middleware on the server).

Categories

Resources