How to send cookie automatically from front-end with httpOnly - node.js

I have a BE which is validating my sign in data then I am storing them in cookies like below:
exports.signIn = async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({
email: email,
});
if (user === null) res.status(400).json({ error: 'No such user' });
let isCorrectUser = await user.authenticatePassword(password);
if (isCorrectUser) {
const token = jwt.sign({ id: user._id }, process.env.SECRET);
// setting httpsOnly:true so that it is automatically sent from front-end
res.cookie('token', token, { httpOnly: true });
res.json({
message: 'User has been successfully logged in',
body: {
token: token,
user: {
id: user._id,
email: user.email,
role: user.role,
},
},
});
} else {
res.status(400).json({ message: 'Incorrect password' });
}
};
I read that if we use httpOnly while making a post request we dont need to send the header with bearer token.
this is my auth helper in front-end to fetch users(only if signed in)
const newData = await fetch(
`http://localhost:3001/api/user/${userData.data.body.user.id}`,
{ credentials: 'include' } // could also try 'same-origin'
);
But I need to specifically set bearer token from sign in explicitly in the above request for it to work. I have read a lot of similar questions but it did not work. Please advise me how to work around.

Related

multiple responses Cannot set headers after they are sent to the client

I wrote the following signup function.
It works fine with Postman but when I've added the code between "//send the cookie with the token" and "// end", I got this error message : "(node:11748) UnhandledPromiseRejectionWarning: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client".
From what I saw here in stackoverflow, this error occurs because of multilple res. but I can't find how to rearrange the code so that I avoid this error.
exports.signup = async(req, res) => {
const { firstName, familyName, email, password, role } = req.body;
console.log("image", req.file);
try {
const user = await User.findOne({ attributes: ['email'], where: { email: email } });
if (user) {
fs.unlinkSync(req.file.path);
return res.status(409).send('This email already exists!');
} else {
const hashPass = await bcrypt.hash(password, 10);
const userObject = {
firstName: firstName,
familyName: familyName,
email: email,
password: hashPass,
role: role,
photoUrl: req.file ? `${req.protocol}://${req.get('host')}/images/${req.file.filename}` : null,
};
console.log("photo", userObject.photoUrl);
console.log("userObject", userObject);
const createdUser = await User.create(userObject);
const newToken = jwt.sign({ userId: user.id },
process.env.COOKIE_KEY, { expiresIn: "24h" }
);
const newCookie = { token: newToken, userId: createdUser.id };
const cryptedToken = cryptojs.AES.encrypt(JSON.stringify(newCookie), process.env.COOKIE_KEY).toString();
res.cookie('snToken', cryptedToken, {
httpOnly: true,
maxAge: 86400000 // 24h
});
res.status(200).send({ message: 'The user is successfully connected!', data: createdUser, cryptedToken: cryptedToken });
}
} catch (error) {
return res.status(500).send({ error: 'An error has occured while trying to sign up!' });
}
}
It's not related to "multiple request". In a successful case (also on error), you wrote status more than once.
for example in a successful case:
first you returned 201 after creating the user (which returned a response to the client)
res.status(201).send({message: User ${req.userObject.firstName} ${req.userObject.familyName} was registred }, createdUser);
and then
and here you tried to send another response at the end of the request
res.status(200).send({ message: 'The user is successfully connected!', data: user, cryptedToken: cryptedToken });
and you got the right error Cannot set headers after they are sent to the client, since it already returned 201 after Creation.
try to do something like:
try {
const hashPass = await bcrypt.hash(password, 10);
const userObject = { firstName: firstName,
familyName: familyName,
email: email,
password: hashPass,
role: role,
photoUrl: req.file ? `${req.protocol}://${req.get('host')}/images/${req.file.filename}` : null,
};
console.log("photo", userObject.photoUrl);
console.log("userObject", userObject);
const createdUser = await User.create(userObject);
//send the cookie with the token
const newToken = jwt.sign({ userId: user.id },
process.env.COOKIE_KEY, { expiresIn: "24h" }
);
const newCookie = { token: newToken, userId: user.id };
const cryptedToken = cryptojs.AES.encrypt(JSON.stringify(newCookie), process.env.COOKIE_KEY).toString();
res.cookie('snToken', cryptedToken, {
httpOnly: true,
maxAge: 86400000 // 24h
});
res.status(200).send({ message: 'The user is successfully connected!', data: user, cryptedToken: cryptedToken });
}
catch (error) {
return res.status(500).send({ error: 'An error has occured while trying to sign up!' });
}

Passing jwt token/userid from `app.post()` to the `app.get()` route

This is my app.post()which gets form data from client.
app.post('/api/login', async (req, res) => {
const { emailid, password } = req.body
const user = await User.findOne({ emailid }).lean()
if (!user) {
return res.json({ status: 'error', error: " Invalid username/Password" })
}
if (bcrypt.compare(password, user.password)) {
const token = jwt.sign({ id: user._id, emailid: user.emailid }, 'secret', { expiresIn: '24h' })
return res.json({ status: 'ok', data: token, user_id: user._id })
}
res.json({ status: 'error', error: " Invalid username/Password" })
})
I need to pass the jwt token or the user_id to my
app.get('/', (req,res)=>{
res.render('index')
})
For this, you will need to create an authentication middleware that will check your request headers for a jwt token, which you can then decode to get the user_id or any other data that you passed to it during encryption. A sample middleware can look like the one below
const isAuth = (req) => {
const authorization = req.headers["authorization"];
if (!authorization) throw new Error("You need to log in");
const token = authorization.split(" ")[1];
const { user_id} = verify(token, process.env.ACCESS_TOKEN_SECRET);
return {user_id, token};
};
After you setup your authorization middleware, you can then go ahead and use it in your routes like so
app.get('/', (req,res)=>{
const {token, user_id) = isAuth(req);
//use token and user_id here or throw error if not available in case this is a protected route
res.render('index')
})

How to get logged in user? req.user is undefind

How to get logged in user in express app. I want to know witch user create post. This is my Post.js model:
const postsSchema = mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User',
},
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
image: {
type: String,
required: true,
},
category: {
type: String,
required: true,
},
numLikes: {
type: Number,
required: true,
default: 0,
},
comments: [commentSchema],
},
{
timestamps: true,
}
);
This is my authUser function where i log in user with email and password:
const authUser = async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (user && (await user.matchPassword(password))) {
res.json({
_id: user._id,
name: user.name,
email: user.email,
isAdmin: user.isAdmin,
token: generateToken(user._id),
});
} else {
res.status(401);
throw new Error('Invalid email or password');
}
};
generateToken function is JWT:
import jwt from 'jsonwebtoken';
const generateToken = id => {
return jwt.sign({ id }, 'abc123', {
expiresIn: '30d',
});
};
export default generateToken;
When i create post i want to know user who created it, this is my create post function:
const createPost = async (req, res) => {
const post = new Post({
user: req.user._id,
title: 'Sample Title',
description: 'Sample Desc',
image: '/images/sample.jpeg',
category: 'Sample Category',
numLikes: 0,
});
const createPost = await post.save();
res.status(201).json(createPost);
};
When i try to create post i got this error in console:
UnhandledPromiseRejectionWarning: TypeError: Cannot read property '_id' of undefined.
I can log in with postman, i can register, i can get user by id. How to tell my app Hey i am logged in user and have access to req.user object
You need to have the client send the token back to you, which you then validate (typically via a middleware affecting some section of endpoints so you don't have to call a validation function in individual endpoints).
If instead, express is also your front end, then you need to use a library like express-session https://www.npmjs.com/package/express-session to manage cookies. A good example is available on their page:
// Use the session middleware
app.use(session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }}))
// Access the session as req.session
app.get('/', function(req, res, next) {
if (req.session.views) {
req.session.views++
res.setHeader('Content-Type', 'text/html')
res.write('<p>views: ' + req.session.views + '</p>')
res.write('<p>expires in: ' + (req.session.cookie.maxAge / 1000) + 's</p>')
res.end()
} else {
req.session.views = 1
res.end('welcome to the session demo. refresh!')
}
})
Otherwise you've sent the token to client and done nothing with it.
Do you need of a middleware like this:
module.exports = (req, res, next) => {
// Authorization token example: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiaWQiOiIxIiwiaWF0IjoxNTE2MjM5MDIyfQ.dYo0kOIhum5mMTRV8CAn8gQ_6aqoDQLE--vCZD4E-fg
const { authorization } = req.headers
if (!authorization) return res.send({ message: 'Token not provided', code: 400 })
const [ schema, token ] = authorization.split(' ')
if (schema !== 'Bearer') return res.send({ message: 'Token is bad formated', code: 400 })
if (!token) return res.send({ message: 'Invalid token', code: 400})
jwt.verify(token, 'abc123', (error, decoded) => {
if (error) return res.send({ message: error.message, code: 401})
req.userId = decoded.id
})
next()
}
Hope this is helpful for you.
The first thing you should do is to send token back to the client or attach cookies to your response.
After which you set up a middleware that will check cookies or token in your case using jwt.verify(token, jwtSecret). That will return the id and all other things you stored in the token, then you then store them in req.user, where you will be able to access the details later.
//if you stored token in cookies -
const {accessToken} = req.signedCookies
const payload = isTokenValid(accessToken) //verify the token
req.user = payload.user;
return next();
//if you stored in auth header
const bearerToken = req.headers.authorization
//bearerToken = "Bearer token"
const token = bearerToken.split(" ").join(",")[1]
//verify the token using jwt
const payload = isTokenValid(token)
req.user = payload
return next()

JWT expired, unexpected response in nodejs API

i have a problem when i try to use a private api in my node.js server, This is the process that i am following
SignUp (if the user doesn't have an account) or logIn (already have an account).
It generates the token and i pass it in the header res.header('access_token': token)
I copy this token and paste it in my private api in the header section (i'm using postman to test it for now) in order to verify if the user is logged in.
In my route, i use a middleware to turn it private, validating if the user can use the resource jwt.verify(authorization[1], process.env.SEED_AUTH) (SEED_AUTH is my token secret stored in my server)
Here is when i have the error, the middleware is failling to verify the user and throw me this error jwt expired
This is my route
const UsersController = require('../controllers/users.controller')
const AuthorizationMiddleware = require('../middlewares/authorization.middleware')
exports.routesConfig = (app) => {
app.post('/api/users',[
AuthorizationMiddleware.verifyValidJWT
], UsersController.insert)
}
This is my middleware
const jwt = require('jsonwebtoken')
require('../config/env.config')
exports.verifyValidJWT = (req, res, next) => {
if (req.headers['access-token']) {
try {
let authorization = req.headers['access-token'].split(' ');
if (authorization[0] !== 'Bearer') {
return res.status(401).json({
ok: false,
err: "Unauthorized, Need a valid token"
});
} else {
console.log(authorization[1]);
req.jwt = jwt.verify(authorization[1], process.env.SEED_AUTH);
return next();
}
} catch (error) {
return res.status(403).json({
ok: false,
err: "Forbidden, need a valid token -> " + error.message
});
}
} else {
return res.status(401).json({
ok: false,
err: "Need to recieve a valid token"
});
}
}
And finally the API UsersController.insert
What i'm trying to do with this api is to create a new user.
For a better understanding this is my LOGIN API
const User = require('../models/users.model')
const { signUpValidation, logInValidation } = require('../middlewares/auth.validation.data')
const bcrypt = require('bcrypt')
const jwt = require('jsonwebtoken')
exports.logIn = async (req, res) => {
let body = req.body;
//Validation
const { error } = logInValidation(body)
if (error) {
return res.status(400).json({
ok: false,
err: error.details[0].message
})
}
//Check if the email already exists in the database
const user = await User.findOne({ email: body.email })
if (!user) {
return res.status(400).json({
ok: false,
err: "Invalid Email or password!"
})
}
const validPass = await bcrypt.compareSync(body.password, user.password)
if (!validPass) {
return res.status(400).json({
ok: false,
err: "Invalid Email or password!"
})
}
const token = jwt.sign(
{
_id: user._id,
email: user.email
},
process.env.SEED_AUTH,
{
expiresIn: process.env.TOKEN_EXPIRY
}
)
res.header('access-token', token).json({
ok: true,
user: {
id: user._id,
email: user.email
}
});
}
SignUp and LogIn validation
I use these middleware to verify if it is a valid email, and the name with a minimum number of letters...
My process.env.TOKEN_EXPIRY is set to 300 (i understand that, it is in seconds), i've tried with bigger number though
(The API works without the middleware).
What would be the problem that i am not seeing. Thanks for your help.
process.env variables are as string and not as a number. According to the jsonwebtoken documentation, string is considered as milliseconds by default and number is counted as seconds by default. So change TOKEN_EXPIRY to 300000 from 300

How to add a new key and value to existing document in MongoDB

I want to add the refreshtoken to the user info when the user logs in. So I can get the refreshtoken and pass it in my cookies.
Code:
router.post("/login", async (req, res) => {
const user = await User.findOne({ email: req.body.email });
if (!user) res.status(400).send({ msg: "User does not exists" });
const validPass = await compare(req.body.password, user.password);
if (!validPass) {
return res.status(400).json({ msg: "email or password is incorrect" });
}
const accesstoken = createAccessToken(user._id);
const refreshtoken = createRefreshToken(user._id);
//create different versions of the refresh token
// put the refreshtoken in the database
User.update(
{ id: user._id },
{
$set: {
refreshtoken: refreshtoken,
},
},
);
let userToken = user.refreshtoken;
userToken = refreshtoken;
//send token. Refreshtoken as a cookie and accesstoken as a regular
//response
//YOU HAVE ALREADY SAID IN THE SENDACCESTOKEN FUNCTION THAT YOU WOULD SEND THE MAIL ALSO
sendRefreshToken(res, refreshtoken);
sendAccessToken(req, res, user._id, accesstoken);
});

Resources