Building out some basic login functionality with JWT's and I am running into an error with setting the cookie... Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
From what I have seen it is because of the middleware going to next and not ending the previous... I just tried res.end(data) while assigning the constant data to the json response. Any advice?
router.post('/users/login', async (req, res) => {
try {
let userInput = {
email: req.body.email,
password: req.body.password
}
const userCheck = await User.findOne({ email: req.body.email })
if (!userCheck) {
res.json("No User with that email!")
} else {
const valid = await bcrypt.compare(req.body.password, userCheck.password)
if (!valid) {
res.json("Invalid password for " + userCheck.email)
} else {
try {
const refreshToken = jsonwebtoken.sign({ userId: userCheck._id }, process.env.REFRESH_TOKEN_SECRET, { expiresIn: '7d' })
const accessToken = jsonwebtoken.sign({ userId: userCheck._id, }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' })
res.json("AccessToken: " + accessToken)
res.cookie("bearer", refreshToken, {
httpOnly: true
})
} catch (err) {
res.json("Error: " + err)
}
}
}
} catch (err) {
res.status(400).json("Error: " + err)
}
})
You just have to call res.cookie() BEFORE you send the response with res.json(). res.cookie() sets the cookie header for the response and that has to be in place before the response is sent. When you call res.json() all headers that have already been configured are sent with the response and you can no longer set any more headers because the response will have already been sent and you only get to send one response.
So, change this:
try {
const refreshToken = jsonwebtoken.sign({ userId: userCheck._id }, process.env.REFRESH_TOKEN_SECRET, { expiresIn: '7d' })
const accessToken = jsonwebtoken.sign({ userId: userCheck._id, }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' })
res.json("AccessToken: " + accessToken)
res.cookie("bearer", refreshToken, {
httpOnly: true
})
} catch (err) {
res.json("Error: " + err)
}
to this:
try {
const refreshToken = jsonwebtoken.sign({ userId: userCheck._id }, process.env.REFRESH_TOKEN_SECRET, { expiresIn: '7d' })
const accessToken = jsonwebtoken.sign({ userId: userCheck._id, }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' })
res.cookie("bearer", refreshToken, { // <== This goes before res.json()
httpOnly: true
})
res.json("AccessToken: " + accessToken)
} catch (err) {
res.json("Error: " + err)
}
Related
I'm trying to set the cookie from my express backend to my nextjs front end using the response.setHeader, but it's not working, I get my json response but no cookies are set, I used postman to make some tests and in postman it did set the cookies as it should be.
NextJs version is 13.0.2, express version is 4.18.2 and cookie version is 0.5.0
My express server:
export const loginUser = async (req: Request, res: Response) => {
const { email, password } = req.body;
//limpa os cookies
res.clearCookie("accessToken", { httpOnly: true });
if (email !== "" && password !== "") {
try {
const foundUser: any = await prisma.users.findFirst({
where: {
email,
password,
},
});
try {
const accessToken = jwt.sign(
{ id: foundUser.id, email },
"dspaojdspoadsaodksa",
{
expiresIn: "10s",
}
);
const refreshToken = jwt.sign(
{ id: foundUser.id, email },
"dsaoindsadmnsaosda",
{
expiresIn: "50s",
}
);
const savedRefresh = await prisma.refresh_token.upsert({
where: {
users_id: foundUser.id,
},
update: {
access_token: accessToken,
refresh_tk: refreshToken,
users_id: foundUser.id,
},
create: {
access_expires_in: "",
refresh_expires_in: "",
access_token: accessToken,
refresh_tk: refreshToken,
users_id: foundUser.id,
},
});
res.setHeader(
"Set-Cookie",
cookie.serialize("accessToken", accessToken, {
maxAge: 1000 * 60 * 15, //15 minutes
httpOnly: true, // The cookie only accessible by the web server
})
);
res.json({ accessToken, user: { email }, ok: true });
} catch (error) {
console.log("refresh cant be created");
res.send({ message: "refresh cant be created" });
}
} catch (error) {
res.send({ message: "user not found" });
}
} else {
res.json({ message: "token creation failed" });
}
};
My nextJs front-end:
const handleLogin = async (event: React.SyntheticEvent) => {
event.preventDefault();
const email = (event?.target as any).email.value;
const password = (event?.target as any).password.value;
if (email === "" || password === "") {
setError(true);
return;
}
const data = {
email,
password,
};
const resp = await fetch("http://localhost:5000/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
const userData = await resp.json();
console.log(userData);
};
I have been trying to set cookies in the browser from nodejs backend trough API with React
and it doesn't want to set them. It's not returning response and it doesn't give me any errors. Does this client.verifytoken function cause the issue? Can you please help?
Nodejs
export const googleAuth = async (req, res) => {
const {tokenId} = req.body
client.verifyIdToken({idToken: tokenId, audience: process.env.GOOGLE_CLIENT_ID}).then((response) => {
const {email_verified, name, email} = response.payload
console.log(response.payload)
if (email_verified) {
Users.findOne({where: {email: email}}).then(user => {
if (user) {
try {
const userId = user.id
console.log('user id', userId)
const refreshToken = jwt.sign({userId}, process.env.REFRESH_TOKEN_SECRET, {expiresIn: '1d'})
Users.update({refreshToken: refreshToken}, {where: {id: userId}})
res.cookie('refreshToken', refreshToken, {
httpOnly: false,
maxAge: 24 * 60 * 60 * 1000,
});
} catch (err) {
console.log(err)
}
} else {
try {
const salt = bcrypt.genSaltSync(2);
const hashPassword = bcrypt.hashSync(email + process.env.ACCESS_TOKEN_SECRET, salt);
const refreshToken = jwt.sign({email}, process.env.REFRESH_TOKEN_SECRET, {expiresIn: '1d'})
console.log('refresh token', refreshToken)
Users.create({
name: name,
email: email,
password: hashPassword,
refresh_token: refreshToken,
verified: true
})
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000,
});
} catch (err) {
console.log(err)
}
}
})
}
})
}
Reactjs
const responseSuccessGoogle = async (response) => {
try {
console.log(response)
let result = await axios.post('http://localhost:5000/google-login', {tokenId: response.tokenId},{withCredentials:true})
setAuth(result.data != null)
navigate('/profile')
console.log(result.data)
} catch (error) {
console.log(error)
}
}
res.cookie() doesn't send the response, but only sets the cookie in response causing halt state in your case. You need to send response back either via res.send() or res.end(). You should also send a proper response with error code back to client instead of logging it only, as this would also halt the request. Following code should send response with empty body and send response with error code 500 in case of error.
export const googleAuth = async (req, res) => {
const {tokenId} = req.body
client.verifyIdToken({idToken: tokenId, audience: process.env.GOOGLE_CLIENT_ID}).then((response) => {
const {email_verified, name, email} = response.payload
console.log(response.payload)
if (email_verified) {
Users.findOne({where: {email: email}}).then(user => {
if (user) {
try {
const userId = user.id
console.log('user id', userId)
const refreshToken = jwt.sign({userId}, process.env.REFRESH_TOKEN_SECRET, {expiresIn: '1d'})
Users.update({refreshToken: refreshToken}, {where: {id: userId}})
res.cookie('refreshToken', refreshToken, {
httpOnly: false,
maxAge: 24 * 60 * 60 * 1000,
});
res.send();
} catch (err) {
console.log(err)
res.status(500).send()
}
} else {
try {
const salt = bcrypt.genSaltSync(2);
const hashPassword = bcrypt.hashSync(email + process.env.ACCESS_TOKEN_SECRET, salt);
const refreshToken = jwt.sign({email}, process.env.REFRESH_TOKEN_SECRET, {expiresIn: '1d'})
console.log('refresh token', refreshToken)
Users.create({
name: name,
email: email,
password: hashPassword,
refresh_token: refreshToken,
verified: true
})
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000,
});
res.send();
} catch (err) {
console.log(err)
res.status(500).send()
}
}
})
}
})
}
i´m creating a Authentication page with React and Express. I'm using JWT too.
I´ve made this route in the back:
server.js
...
app.use(
cookieSession({
name: "prode_session",
secret: "MIOURI_PRODE_SECRET", //add to .env variable
httpOnly: false,
})
);
app.use(cors());
...
auth.routes.js
app.post("/signin", controller.signin);
user.routes.js
app.get(
"/user",
[authJwt.verifyToken],
(req, res) => res.send(true)
)
auth.controller.js
exports.signin = async (req, res) => {
const user = await Users.findOne({
where: { email: req.body.email },
});
try {
if (!user) {
return res.status(404).send({ message: "User Not found." });
}
const passwordIsValid = bcrypt.compareSync(
req.body.password,
user.password
);
if (!passwordIsValid) {
return res.status(401).send({
message: "Invalid Password!",
});
}
const token = jwt.sign({ id: user.id }, config.secret, {
expiresIn: 84000, //24hours
});
req.session.token = token;
console.log(req.session);
return res.status(200).send({
isLogged: true,
id: user.id,
email: user.email,
suscripcion: user.suscripcion,
preference_id: user.preference_id,
token,
});
} catch (error) {
console.log(error);
}
};
authJWT.js
verifyToken = async (req, res, next) => {
let token = req.session.token;
console.log(`THIS IS THE TOKEN: ${token}`);
if (!token) {
return res.status(403).send({
message: "No token provided",
});
}
jwt.verify(token, config.secret, (err, decoded) => {
if (err) {
console.log(err);
return res.status(401).send({
message: "Unauthorized!",
});
}
req.id = decoded.id;
next();
});
};
const authJwt = { verifyToken };
module.exports = authJwt;
When I test this with POSTMAN, it works Ok, I mean, if first I try to make the GET request, the response is "No token provided", but if I signin first, generate the token and then make the GET request, I get true.
The problem is when I try to implement this in the front.
I have this Login component in React in which I make a POST request with the credentials:
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await fetch("http://localhost:3000/signin", {
method: "POST",
mode: "cors",
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
body: JSON.stringify({
email,
password,
}),
});
const data = await response.json();
console.log(data);
if (data.isLogged && data.suscripcion === true && data.token) {
await tokenAvailable()
//navigate(`/masthead/${email}&${data.isLogged}&${data.id}`);
} else if (data.isLogged && data.suscripcion === false) {
navigate("/suscripcion", {
state: { preference_id: data.preference_id },
});
} else {
window.alert("Invalid Login");
}
} catch (error) {
console.log(error);
}
};
async function tokenAvailable() {
const user = await fetch("http://localhost:3000/user", {
method: "GET",
mode: "cors",
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
});
const response = await user.json();
setUser(await response);
console.log(await response);
return response;
}
When I make the POST, the GET request is executed (tokenAvailable function) after receiving the response, but I receive "No token Provided" while I expect to receive "true" as in Postman.
From what I debug, the authJWT.js file, is not receiving nothing from the req.session.token.
When I compare the headers from postman and the browser, in postan the SET-cookie key appears, but in the browser not.
postman:
browser:
I need some help here. I´ve been strugling with this for almost 3 days.
I found a solution for this. Apparently, the HttpOnly Cookie approach works if the React app and the back-end server hosted in same domain. So we need to use http-proxy-middleware for local development.
I´ve tried to install the http-proxy-middleware but a lot of errors came, so I decided to store de JWT in the localstorage.
I have a controllerfile where I use passport.authenticate. I declare my payload and sign my token now i need the info declared in the payload in another file so I could use them in my sql request.
Here's the code for the login auth :
login: (req, res, next) => {
console.log(" login");
passport.authenticate("local", { session: false }, (error, user) => {
console.log("executing callback auth * from authenticate for local strategy ");
//if there was an error in the verify callback related to the user data query
if (error || !user) {
next(new error_types.Error404("Email ou Mot de passe invalide !"))
}else {
console.log("*** Token generation begins ***** ");
console.log(user)
const payload = {
sub: user.id,
exp: Date.now() + parseInt(process.env.JWT_LIFETIME),
email: user.email,
name: user.prenom,
lastName: user.nom,
type:user.type,
};
const token = jwt.sign(JSON.stringify(payload), process.env.JWT_SECRET, {algorithm: process.env.JWT_ALGORITHM});
res.json({ token: token,type: user.type,userid:user.id });//added userid
}
})(req, res);
}
Now in my other file i need to get the user.id and user.type so that i could use them in my request :
const createProp=(req, res, next) => {
let con=req.con
let { xx,yy } = req.body;
con.query('INSERT INTO tab1
(xx,yy,user_id,user_type ) VALUES ($1, $2, $3, $4) ',[xx,yy,user_id,user_type],
(err, results) => {
if (err) {
console.log(err);
res.status(404).json({error: err});
}
else
{res.status(200).send(`success`)}
}
);
}
in my frontend VUEJS this is my file:
import router from '#/router'
import { notification } from 'ant-design-vue'
import JwtDecode from "jwt-decode";
import apiClient from '#/services/axios'
import * as jwt from '#/services/jwt'
const handleFinish = (values) => {
const formData = new FormData()
for (var key of Object.keys(formState)) {
formData.append(key, formState[key])//here im appending some fields in my
//form i have more than just xx,yy files i just put them as an
//example
}
const token = localStorage.getItem("accessToken");
var decoded = JwtDecode(token);
console.log(decoded)
formData.append('user_id',decoded.sub)
formData.append('user_type',decoded.type)
fileListFaisabilite.value.forEach((file) => {
formData.append('xx', file)
})
fileListEvaluation.value.forEach((file) => {
formData.append('yy', file)
})
// store.dispatch('user/PROPOSITION', formData)
}
methods:{
PROPOSITION({ commit, dispatch, rootState }, formData ) {
commit('SET_STATE', {
loading: true,
})
const proposition=
mapAuthProviders[rootState.settings.authProvider].proposition
proposition(formData)
.then(success => {
if (success) {
notification.success({
message: "Succesful ",
description: " form submited!",
})
router.push('/Accueil')
commit('SET_STATE', {
loading: false,
})
}
if (!success) {
commit('SET_STATE', {
loading: false,
})
}
})
return apiClient
.post('/proposition', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
.then(response => {
if (response) {
return response.data
}
return false
})
.catch(err => console.log(err))
},
},
What im looking for is how i can store in my database the userid and usertype using insertinto sql request.
You can set user data in your jwt sign function without stringify method:
const payload = {
sub: user.id,
exp: Date.now() + parseInt(process.env.JWT_LIFETIME),
email: user.email,
name: user.prenom,
lastName: user.nom,
type: user.type // <-- Add this
};
const token = jwt.sign(
payload, // Don't use JSON.stringify
process.env.JWT_SECRET,
{algorithm: process.env.JWT_ALGORITHM}
);
And access user info:
jwt.verify(token, process.env.JWT_SECRET, (err, payload) => {
if (err) {
// Handle error
}
// Get some data
let user_id = payload.sub;
let user_type = payload.type;
console.log(user_id, user_type);
next();
});
The vue file:
PROP({ commit, dispatch, rootState }, payload ) {
commit('SET_STATE', {
loading: true,
});
const prop = mapAuthProviders[rootState.settings.authProvider].prop
prop(payload)
.then(success => {
if (success) {
// success contains user information and token:
const { token, userid, type } = success;
// Save to localStorage (Optional)
localStorage.setItem("accessToken", token);
localStorage.setItem("userid", userid);
localStorage.setItem("type", type);
// This not works if don't have a JWT SEED
// var decoded = JwtDecode(token);
commit('SET_STATE', {
user_id: userid,
user_type: type,
})
//dispatch('LOAD_CURRENT_ACCOUNT')
notification.success({
message: "Succesful ",
description: " form submited!",
})
router.push('/Home')
commit('SET_STATE', {
loading: false,
})
}
if (!success) {
commit('SET_STATE', {
loading: false,
})
}
})
},
The api call file:
export async function prop(payload) {
try {
const response = await apiClient.post('/prop', payload, {
headers: { 'Content-Type': 'multipart/form-data'},
});
if (response) {
return response.data;
}
} catch (err) {
console.log(err);
}
return false;
}
I am trying to configure my JWT through http-only cookies
const signIn = async (req, res) => {
const { email } = req.body;
try {
const user = await User.findOne({ email });
const access_token = jwt.sign({ id: user._id }, jwtSecret, {
expiresIn: token_expiry_time,
});
const refresh_token = jwt.sign({ id: user._id }, jwtRefreshSecret, {
expiresIn: refresh_token_expiry_time,
});
res.cookie('access_token', access_token, { httpOnly: true });
res.cookie('refresh_token', refresh_token, { httpOnly: true });
return res.status(200).json({
status: true,
data: {
user: {
id: user._id,
},
},
});
} catch (err) {
Server.serverError(res, err);
}
};
but at the client-side it refuses to set the cookie in the browser, it returns the error "cannot set cookie because same-site is not 'None' ", After setting sameSite:'None' on the server side, it then gave the error "same-site set to 'None' needs to have a 'Secure' field", I then set secure:true on the backend but it doesn't work because I am not using https for development.
client-side code
const onSubmit = (cred) => {
setLoading(true);
restAPI
.post('auth/signin', cred)
.then(({ data }) => {
setLoading(false);
setError(false);
console.log(data.data);
})
.catch((err) => {
setLoading(false);
setError({
status: true,
message: err.response.data.message,
});
});
};
how can I solve this?