I was following a tutorial at Authentication in NodeJS With Express and Mongo - CodeLab #1
I got everything to work perfectly, but the tutorial does not address how to log out a user.
From what I can tell, the session is being saved on Mongoose Atlas, which is the database I am using. When I log a user in with Postman, I get a token back. But I am not sure how to configure the /logout route.
Here is my code:
//routes/user.js
const express = require("express");
const { check, validationResult } = require("express-validator");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const router = express.Router();
const auth = require("../middleware/auth");
const User = require("../models/User");
/**
* #method - POST
* #param - /signup
* #description - User SignUp
*/
//Signup
router.post(
"/signup",
[
check("username", "Please Enter a Valid Username")
.not()
.isEmpty(),
check("email", "Please enter a valid email").isEmail(),
check("password", "Please enter a valid password").isLength({
min: 6
})
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
errors: errors.array()
});
}
const {
username,
email,
password
} = req.body;
try {
let user = await User.findOne({
email
});
if (user) {
return res.status(400).json({
msg: "User Already Exists"
});
}
user = new User({
username,
email,
password
});
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password, salt);
await user.save();
const payload = {
user: {
id: user.id
}
};
jwt.sign(
payload,
"randomString", {
expiresIn: 10000
},
(err, token) => {
if (err) throw err;
res.status(200).json({
token
});
}
);
} catch (err) {
console.log(err.message);
res.status(500).send("Error in Saving");
}
}
);
// Login
router.post(
"/login",
[
check("email", "Please enter a valid email").isEmail(),
check("password", "Please enter a valid password").isLength({
min: 6
})
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
errors: errors.array()
});
}
const { email, password } = req.body;
try {
let user = await User.findOne({
email
});
if (!user)
return res.status(400).json({
message: "User Not Exist"
});
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch)
return res.status(400).json({
message: "Incorrect Password !"
});
const payload = {
user: {
id: user.id
}
};
jwt.sign(
payload,
"randomString",
{
expiresIn: 3600
},
(err, token) => {
if (err) throw err;
res.status(200).json({
token
});
}
);
} catch (e) {
console.error(e);
res.status(500).json({
message: "Server Error"
});
}
}
);
// router.route("/logout").get(function (req, res, next) {
// if (expire(req.headers)) {
// delete req.user;
// return res.status(200).json({
// "message": "User has been successfully logged out"
// });
// } else {
// return next(new UnauthorizedAccessError("401"));
// }
// });
router.get("/me", auth, async (req, res) => {
try {
// request.user is getting fetched from Middleware after token authentication
const user = await User.findById(req.user.id);
res.json(user);
} catch (e) {
res.send({ message: "Error in Fetching user" });
}
});
router.get('/logout', isAuthenticated, function (req, res) {
console.log('User Id', req.user._id);
User.findByIdAndRemove(req.user._id, function (err) {
if (err) res.send(err);
res.json({ message: 'User Deleted!' });
})
});
module.exports = router;
function isAuthenticated(req, res, next) {
console.log("req: " + JSON.stringify(req.headers.authorization));
// if (!(req.headers && req.headers.authorization)) {
// return res.status(400).send({ message: 'You did not provide a JSON web token in the authorization header' });
//}
};
///middleware/auth.js
const jwt = require("jsonwebtoken");
module.exports = function (req, res, next) {
const token = req.header("token");
if (!token) return res.status(401).json({ message: "Auth Error" });
try {
const decoded = jwt.verify(token, "randomString");
req.user = decoded.user;
next();
} catch (e) {
console.error(e);
res.status(500).send({ message: "Invalid Token" });
}
};
///models/User.js
const mongoose = require("mongoose");
const UserSchema = mongoose.Schema({
username: {
type: String,
required: true
},
email: {
type: String,
required: true
},
password: {
type: String,
required: true
},
createdAt: {
type: Date,
default: Date.now()
}
});
// export model user with UserSchema
module.exports = mongoose.model("user", UserSchema);
So my question is, how can I implement a /logout route so that if the user clicks the logout button and invokes that route, their token is destroyed. I am only asking about the back-end part. I can handle using axios.
Thanks.
From what I see, you are not saving any session data or storing tokens anywhere - which is great. You are simply appending the token to your headers in requests to the API.
So the only thing you can do is possibly expire the token in the /logout route
and then ensure you delete the token on the client - could be localStorage, sessionStorage etc - your client code needs to kill the token so it cannot be included again.
Side note:
You are not extending the token lifetime anywhere, so even if the user keeps interacting on the website, the token expiration is not being updated. You will need to manually refresh the token/generate a new token to have a sliding expiration.
I would suggest you save the token in cookies rather. Set the cookie to HttpOnly, Secure, and specify the domain. This is far more secure and will allow you to also expire the cookie from the API. If any scripts you include get compromised, they can access all your users’ tokens easily.
Example:
import {serialize} from 'cookie';
import jsend from 'jsend';
...
const token = jwt.sign(
{
id: validationResult.value.id // whatever you want to add to the token, here it is the id of a user
},
privateKeyBuffer,
{
expiresIn: process.env.token_ttl,
algorithm: 'RS256'
});
const cookieOptions = {
httpOnly: true,
path: '/',
maxAge: process.env.token_ttl,
expires: new Date(Date.now() + process.env.token_ttl),
sameSite: process.env.cookie_samesite, // strict
domain: process.env.cookie_domain, // your domain
secure: process.env.cookie_secure // true
};
const tokenCookie = await serialize('token', token, cookieOptions);
res.setHeader('Set-Cookie', [tokenCookie]);
res.setHeader('Content-Type', 'application/json');
res.status(200).json(jsend.success(true));
Then in logout:
// grab from req.cookies.token and validate
const token = await extractToken(req);
// you can take action if it's invalid, but not really important
if(!token) {
...
}
// this is how we expire it - the options here must match the options you created with!
const cookieOptions = {
httpOnly: true,
path: '/',
maxAge: 0,
expires: 0,
sameSite: process.env.cookie_samesite, // strict
domain: process.env.cookie_domain, // your domain
secure: process.env.cookie_secure // true
};
// set to empty
const tokenCookie = await serialize('token', '', cookieOptions);
res.setHeader('Set-Cookie', [tokenCookie]);
res.setHeader('Content-Type', 'application/json');
res.status(200).json(jsend.success(true));
As you have used JWT the backend will always check 2 things
1. Proper token
2. If time is over for that particular (You should handle this one)
For 2nd point, if the user time is up than from frontend you may delete the token if you have stored the token in localstorage.
For logout when user clicks on logout just delete jwt from localstorage and redirect to login or other page
Related
cookies just keeps adding on and on when I register a new user.
userRoutes.js:
const { registerUser, loginUser, registerVerify } = require("./userController");
const express=require('express')
const router=express.Router()
router
.route('/register')
.post(registerUser)
module.exports=router
userController.js:
const User = require("./userModel");
const validator = require("validator");
const jwt=require('jsonwebtoken')
const sendToken = function (email) {
let data = {
expiresIn: process.env.JWT_EXPIRE_SHORT,
email,
};
let token = jwt.sign(data, process.env.JWT_SECRET_KEY);
return token;
};
exports.registerUser = async (req, res, next) => {
try {
const { userName, email, password, confirmPassword } = req.body;
let user = [
{ userName, email, password, confirmPassword, createdAt: Date.now() },
];
//passwords do not match error
if (password !== confirmPassword) {
throw "Password doesn't match Confirm Password.";
}
//email not valid error
if (!validator.isEmail(email)) {
throw "Email not valid.";
}
//user already logged in error
if (await User.findOne({ email })) {
throw "User already exists. Please login";
}
//create user
await User.create(user);
//assign token
let token = sendToken(email);
//clear cookie
res.status(200).clearCookie('token').cookie(token, 'token', {maxAge: 360000}).json({
message: "Please fill the required text send via email",
token,
});
} catch (err) {
// console.log(err)
res.status(400).json({
message: "Signup Not Successfull",
err,
});
}
};
I've tried:
router
.route('/register',{ credentials: 'same-origin'})
.post(registerUser)
router
.route('/register',{ credentials: 'include'})
.post(registerUser)
router
.route('/register',{ withCredentials: true})
.post(registerUser)
Here is the correct way for creating a cookie:
res.cookie('token', token, {maxAge: 360000});
Note: You don't need to clear and create a new one.
when you send a cookie with the same key it overrides itself.
const router = require("express").Router();
const mongoose = require("mongoose");
const User = require("../models/Users");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
// Route 1: create new user at /api/createuser
router.post("/createuser", async (req, res) => {
try {
console.log(req.body);
const salt = await bcrypt.genSaltSync(10);
const hash = await bcrypt.hashSync(req.body.password, salt);
password = hash;
const user = new User({
name: req.body.name,
email: req.body.email,
password: password,
});
user
.save()
.then(() => {
res.json({ message: "User created successfully" });
})
.catch((err) => {
res.json({ message: "Error: " + err });
});
console.log(password);
} catch (err) {
res.json({ message: err });
}
});
// Route 2: Login user at /api/login
router.post("/login", async (req, res) => {
try {
console.log("login endpoint triggered");
const user = await User.findOne({ email: req.body.email });
if (!user) {
res.json({ message: "User does not exist" });
}
const passwordIsValid = await bcrypt.compare(
req.body.password,
user.password
);
if (!passwordIsValid) {
res.json({ message: "Invalid password" });
} else {
const data = {
id: user._id,
};
const token = await jwt.sign(data, process.env.SECRET);
res.json(token);
}
} catch (error) {
res.json({ message: error });
}
});
module.exports = router;
Whenever I am testing the login endpoint, my app crashes if i try to put incorrect password or unregistered email.
It says Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
I have only sent one response to the client even then it is showing this error.
In the terminal, it is showing error at catch block of login endpoint.
Can anyone look into it and tell me why am i getting this error.
Make sure that when the response is an error, the rest of the middleware is not executed any more. For example, put a return before the res.json statement:
if (!user) {
return res.json({ message: "User does not exist" });
}
Otherwise, the "Invalid password" json might get issued after this one, but two res.jsons in one response lead to the observed error.
I want to make a middleware to check the user. I'm using JWT and cookies for this. I retrieved the cookie and decrypted it (it has been encrypted in the login controller function). Then I used jwt.verify(). But I've received this error message : JsonWebTokenError: jwt malformed.
I've seen that it may mean that the token wasn't "the right formated" one. But I can't figure it out.
checkUser function :
exports.checkUser = async(req, res, next) => {
const cryptedToken = req.cookies.snToken;
console.log("cryptedToken01", cryptedToken); //displays a string consists of 3 parts, separated by /
const token = cryptojs.AES.decrypt(cryptedToken, process.env.COOKIE_KEY).toString();
console.log("token01", token); // displays a longer monolithic string
if (token) {
jwt.verify(token, process.env.COOKIE_KEY, async(err, verifiedJwt) => {
if (err) {
console.log("err inside jwt verify", err); // displays an error mesassage (JsonWebTokenError: jwt malformed)
console.log("res.locals", res.locals); //displays res.locals [Object: null prototype] {}
res.locals.user = null;
res.cookie("snToken", "", { maxAge: 1 });
next();
} else {
let user = await User.findByPk(verifiedJwt.userId);
res.locals.user = user;
next();
}
});
} else {
res.locals.user = null;
next();
}
};
my login function :
exports.login = async(req, res) => {
try {
const user = await User.findOne({ where: { email: req.body.email } });
if (!user) {
return res.status(403).send({ error: 'The login information (email) is incorrect!' });
}
bcrypt
.compare(req.body.password, user.password)
.then((isPasswordValid) => {
if (!isPasswordValid) {
return res.status(403).send({ error: 'The login information (pwd) is incorrect!' });
} else {
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
});
//res.status(200).send({ message: 'The user is successfully connected!', data: user });
res.status(200).send({ message: 'The user is successfully connected!', data: user, cryptedToken: cryptedToken });
}
});
} catch (error) {
res.send({ error: 'An error has occured while trying to log in!' });
}
}
The call of these middlwares in my app.js:
app.get('*', checkUser);
In your current code, you get a Hex encoded ASCII string after decryption
7b22746f6b656e223a2265794a68624763694f694a49557a49314e694973496e523563434936496b705856434a392e65794a3163325679535751694f6a45314c434a70595851694f6a45324e4445314e6a45324d545173496d5634634349364d5459304d5459304f4441784e48302e693670564f486443473456445154362d3749644545536f326251467765394d4b34554a316f363676564334222c22757365724964223a31357d,
which contains your cookie as a stringified JSON.
Instead of toString() after decrypting, which causes the hex encoded output, call toString(cryptojs.enc.Utf8) to get a JSON String and then parse it to an object:
const bytes = cryptojs.AES.decrypt(cryptedToken, process.env.COOKIE_KEY);
const cookie = JSON.parse(bytes.toString(cryptojs.enc.Utf8));
console.log("token", cookie.token);
the result is a correct JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjE1LCJpYXQiOjE2NDE1NjE2MTQsImV4cCI6MTY0MTY0ODAxNH0.i6pVOHdCG4VDQT6-7IdEESo2bQFwe9MK4UJ1o66vVC4
I am trying to create a book tracker with the MERN stack. I want to save some information in session, that is, when user tries to register, he hits a route, where validation happens and an email is sent to check if email really exists. Now the username, email, password and a verification code, are saved in session (which is actually not working), so that the verify route can access it, and save the user to database is the verification code entered by the user is correct which was sent to the email. Now saving of the information is not working, here is my code:
app.js file (entry point)
import dotenv from "dotenv";
dotenv.config();
import express from "express";
import bodyParser from "body-parser";
import cors from "cors";
import mongoose from "mongoose";
import session from "express-session";
// import routes
import authRoutes from "./routes/auth.js";
// db config
mongoose
.connect(process.env.DB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => console.log("DB Connected"))
.catch((err) => console.log(err));
// constants
const app = express();
const port = process.env.PORT || 5001;
// middlewares
app.use(bodyParser.json());
app.use(cors());
app.use(
session({
secret: process.env.SECRET,
resave: true,
saveUninitialized: true,
expires: new Date(Date.now() + 3600000),
proxy: true,
})
);
// using routes
app.use("/api", authRoutes);
app.listen(port, () =>
console.log(`Server is running on http://localhost:${port}/`)
);
routes/auth.js (auth related routes)
import dotenv from "dotenv";
dotenv.config();
import express from "express";
import securePin from "secure-pin";
import { User } from "../models/user.js";
import { transporter } from "../config/nodemailer.js";
const router = express.Router();
function validateEmail(email) {
return /^\w+([\.-]?\w+)*#\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(email);
}
router.post("/register", (req, res) => {
const { username, email, password } = req.body;
//check email matches the pattern
if (!validateEmail(email)) {
return res.json({
status: "error",
message: "Email is not valid",
});
}
// check if email already exists
User.findOne({ email: email }, (err, user) => {
if (user) {
return res.json({
status: "error",
message: "Email already exists, please sign in",
});
}
});
// check username is taken
User.findOne({ username: username }, (err, user) => {
if (user) {
return res.json({
status: "error",
message: "Username already exists, please choose another one",
});
}
});
// check username is atleast 4 characters
if (username.length < 4) {
return res.json({
status: "error",
message: "Username must be atleast 4 characters",
});
}
// check if username contains invalid characters
if (username.match(/[^a-zA-Z0-9_]/)) {
return res.json({
status: "error",
message: "Username must not contain special characters or spaces",
});
}
// check password contains spaces
if (password.match(/\s/)) {
return res.json({
status: "error",
message: "Password must not contain spaces",
});
}
// check password is atleast 6 characters
if (password.length < 6) {
return res.json({
status: "error",
message: "Password must be atleast 6 characters",
});
}
// generate unique token
var charSet = new securePin.CharSet();
charSet.addLowerCaseAlpha().addUpperCaseAlpha().addNumeric().randomize();
const code = securePin.generateStringSync(6, charSet);
// save info in session
req.session.username = username;
req.session.email = email;
req.session.password = password;
req.session.code = code;
req.session.save();
// send token to user's email
const mailOptions = {
from: process.env.EMAIL_ADDRESS,
to: email,
subject: "no reply- book tracker confirmation",
text: `Your verification code is: ${code}.`,
};
transporter.sendMail(mailOptions, (err, info) => {
if (err) {
return res.json({
status: "error",
message: err.message.toString(),
});
}
return res.json({
status: "success",
message: "Email sent",
});
});
});
router.post("/register/verify", (req, res) => {
const { code } = req.body;
const { username, email, password } = req.session;
// check if code matches
if (code !== req.session.code) {
return res.json({
status: "error",
message: "Code is invalid",
});
}
// create user
const user = new User({
username,
email,
password,
});
// save user
// user.save((err) => {
// if (err) {
// return res.json({
// status: "error",
// message: err.message,
// });
// }
// return res.json({
// status: "success",
// message: "Account created successfully!",
// });
// });
console.log(user);
return res.json(user);
});
export default router;
const express = require('express');
const router = express.Router();
const auth = require('../../middleware/auth');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const config = require('config');
const { check, validationResult } = require('express-validator');
const User = require('../../models/User');
// #route Get api/auth
// #desc Test route
// #access Public
router.get('/', auth, async (req, res) => {
try {
const user = await User.findById(req.user.id).select('-password');
res.json(user);
} catch(err) {
console.error(err.message);
res.status(500).send('Server Error')
}
});
// #route POST api/auth
// #desc Authenticate User And Get Token
// #access Public
router.post('/',
[
check('email', 'Please include a valid email').isEmail(),
check('password', 'Password is required').exists()
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array()});
}
const { email, password } = req.body;
try {
// See if user exists
let user = await User.findOne({ email})
if (!user) {
return res
.status(400)
.json({ errors: [ { msg: 'Invalid Credentials Email' } ] });
}
// Make Sure Password matches
const isMatch = await bcrypt.compare(password, user.password);
if(!isMatch) {
return res
.status(400)
.json({ errors: [ { msg: 'Invalid Credentials Password' } ] });
}
const payload = {
user: {
id: user.id
}
}
jwt.sign(
payload,
config.get('jwtSecret'),
{ expiresIn: 360000 },
(err, token) => {
if(err) throw err;
res.json({ token });
}
);
} catch(err) {
console.error(err.message);
res.status(500).send('Server error')
}
});
module.exports = router
In my database I have email and password
in my postman when i make POST request to https://localhost:5000/api/auth
the email is correct however I keep getting password is not correct with this const isMatch = await bcrypt.compare(password, user.password);
if(!isMatch) {
return res
.status(400)
.json({ errors: [ { msg: 'Invalid Credentials Password' } ] });
}
i console logged both password and user.password and it is the same value, i dont understand why !isMatch is keep getting triggered
can anyone help me
The syntax of bcrypt.compare() is:
bcrypt.compare(plaintextPassword, hash)...
The first parameter is plaintext password and second is hash of the real password so it will not be the same value. As you mentioned that your password and user.password is the same value, I guess you forgot to hash user password before saving it to the DB. Please check the docs for more details.