I'm currently creating an application in React/Express and I'm learning how to create sessions. I'm using express-session as it's what everyone recommends but I have unexpected behaviors.
In my route post, the route used during the connection, I try to create a new session for the user but it does not seem to work (no cookie and the session is not created) while my console.log returns the expected information.
router.post('/login', async (req, res) => {
const user = await Users.findOne({where: {Email: req.body.Email}})
if (!user) res.json({error: "User doesn't exist"})
bcrypt.compare(req.body.Password, user.Password).then((match) => {
if (!match) res.json({error: "Wrong password"})
req.session.user = user.dataValues
console.log(req.session)
})
})
In my get route, which is called at every refresh of the page, I realize that the session is empty and a new cookie is created (I don't really know why).
router.get('/login', async (req, res) => {
console.log(req.session)
if (req.session.user) {
res.send({loggedIn: true, user: req.session.user})
} else {
res.send({ loggedIn: false})
}
})
Here is how I set up express-session as well as cors (I read that the problem could come from there but all seems correct).
app.use(cors({
origin: ["http://localhost:3000"],
methods: ["GET", "POST"],
credentials: true //permet d'activer les cookies
}))
app.use(session({
key: "userId",
secret: "foo",
resave: false,
saveUninitialised: true,
cookie: {
expires: 60 * 60 * 24
},
}))
I also read that the problem could come from the API call, I use Axios and I was careful to add the line Axios.defaults.withCredentials = true before the call.
Your router.post("/login", ...) route never sends any response back to the client. An express session works by establishing a cookie with the browser that the browser will send back on future requests. That cookie contains an encrypted session key that is the magic sauce that makes the session possible. When you don't send any response back from the /login POST, then that cookie never gets back to the browser and thus the session cookie can't be sent back on future requests and thus the session does not work.
Instead, the next request coming from the browser will not have a session cookie and thus Express will try to create yet another new empty session.
To fix that part of the issue, send a response back from your POST request:
router.post('/login', async (req, res) => {
const user = await Users.findOne({where: {Email: req.body.Email}})
if (!user) res.json({error: "User doesn't exist"})
bcrypt.compare(req.body.Password, user.Password).then((match) => {
if (!match) res.json({error: "Wrong password"})
req.session.user = user.dataValues;
console.log(req.session)
res.send("some response"); // <== send some response here
}).catch(err => {
// some error handling here
console.log(err);
res.sendStatus(500);
});
});
For, more complete and centralized error handling where you use http status to reflect actual errors, you can do something like this:
class myError extends Error {
constructor(message, status) {
super(message);
this.status = status;
}
}
router.post('/login', async (req, res) => {
try {
const user = await Users.findOne({where: {Email: req.body.Email}})
if (!user) throw new MyError("User doesn't exist", 404) ;
const match = await bcrypt.compare(req.body.Password, user.Password);
if (!match) throw new MyError("Wrong password", 401);
req.session.user = user.dataValues;
console.log(req.session);
res.json({loggedIn: true});
} catch(e) {
const status = e.status || 500;
res.status(status).json({error: e.message});
}
});
Note, I've stopped mixing await with .then() which is not considered good style and then used try/catch and throw to integrate the more comprehensive error handling into one spot.
Related
How is it going?
Currently making my first experiences in developing with node, express, JWT and Login mechanism. I am also using EJS, but this is not part of this discussion.
There are hundreds of tutorials out there on the internet, all of them are good and have helped me so far.
But there is something missing crucially in all of these tutorials. I mean the error handling on the client side.
Maybe I am on the wrong way. But as of now, I could not find a way out.
JWT and login does work so far. Also does the verification of the JWT token.
What is not working is how to catch the 403 error that comes from the VerifyToken middleware. In that case, I would like to redirect the user to the login form again.
Better I give you some code (condensed to the most interesting parts):
server.js
const auth = require("./auth/AuthController.js");
server.use('/auth', auth);
server.use("/", VerifyToken, (req,res,next) => {
res.render("index", { page: "drivers", username: req.userName });
});
AuthController.js
const VerifyToken = require('./VerifyToken.js');
app.post("/login", (req,res,next) => {
conn.query("call sp_GetUserByName(?)", [req.body.username], function (err, row) {
if (err) {
res.status(500).send("DB Error: "+err)
} else {
if(row[0].length == 0){ res.status(200).send("nouser") }
else{
var dbpass = row[0][0].password;
bcrypt.compare(req.body.userpassword, dbpass, function(err, r){
if(err){
console.log("error: "+err);
res.status(500).send("pwcomperr")
}
else{
if(r){
const token = jwt.sign(
{ user_id: row[0][0].ID,
user_name: row[0][0].name },
process.env.jwtpass,
{
expiresIn: "6h",
}
);
return res
.cookie("access_token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
})
.status(200)
.send("pwmatch");
}
else{
res.status(200).send("pwconflict")
}
}
});
}
}
});
});
VerifyToken.js
const jwtpass = process.env.jwtpass;
const VerifyToken = (req, res, next) => {
const token = req.cookies.access_token;
if (!token) {
return res.sendStatus(403);
}
try {
const data = jwt.verify(token, jwtpass);
req.userId = data.user_id;
req.userName = data.user_name;
return next();
} catch {
return res.sendStatus(403);
}
}
module.exports = VerifyToken
As you can see, I return a 403 if a user is not logged in (=no token present) or the token is timed out.
But with this code implementation, I get a 403 returned to the user's browser, saying "Forbidden" (which is correct, of course). Even if I want to access the root of the website.
I do not pass this request via Ajax or such. I only type the URL in the Browser: http://app-server.local:3000.
How is it possible to catch that error and to redirect to /auth/loginForm ?
I mean, I am surely not the first one coding that stuff - how do the others do it?
I have been following the tutorial on creating a test API application from this article. At the end of the article i see a mention its best to encrypt the jwt token for added security so i wen searching for a way to do that as well. I ran into this article and it gives examples of how to encrypt the jwt token with RSA private/public keys.
THIS is where im getting stuck. After i have successfully signed up using the /signup route, i can then use the /login route to get my token. So im assuming this is where i use my private key to encrypt the token before sending it back to the user?
**Made repo public for testing - you will need only to provide a mongoDB connection string in app.js
Im stuck at the encrypt/decrypt portion of the process, any help appreciated.
router.post("/login", async (req, res, next) => {
passport.authenticate("token", async (err, user, info) => {
try {
if (err || !user) {
const error = new Error("An Error occurred");
return next(error);
}
req.login(user, { session: false }, async error => {
if (error) return next(error);
//We don't want to store the sensitive information such as the
//user password in the token so we pick only the email and id
const body = { _id: user._id, email: user.email };
//Sign the JWT token and populate the payload with the user email and id
const token = jwt.sign({ user: body }, PRIV_KEY, { algorithm: 'RS256' });
//Send back the token to the user
return res.json({ token });
});
} catch (error) {
return next(error);
}
})(req, res, next);
});
And then when making calls to the "secure" routes i need to decrypt the token and verify it against the public key?
router.get("/profile", (req, res, next) => {
//We'll just send back the user details and the token
jwt.verify(req.query.token, PUB_KEY, { algorithms: ['RS256'] }, function(err, decoded) {
if (err.name === "TokenExpiredError") {
console.log("Whoops, your token has expired!");
}
if (err.name === "JsonWebTokenError") {
console.log("That JWT is malformed!", err); <------ GET ERROR HERE
}
if (err === null) {
console.log("Your JWT was successfully validated!");
}
// Both should be the same
console.log(decoded);
res.json({
message: "You made it to the secure route",
user: req.user
});
});
});
I don’t have the time to reproduce this. Your login part seems correct. However, you should try to setup protected routes like this, copied and tailored to your needs from your first article:
Setting up middleware to handle jwt decryption, make sure to require it in your app.js or wherever you need to, if you set it up in a separate file. This can be used as a middleware later on in your controllers:
const JWTstrategy = require('passport-jwt').Strategy;
//We use this to extract the JWT sent by the user
const ExtractJWT = require('passport-jwt').ExtractJwt;
//This verifies that the token sent by the user is valid
passport.use(new JWTstrategy({
//secret we used to sign our JWT
secretOrKey : PUB_KEY,
algorithms: ['HS256'],
//we expect the user to send the token as a query parameter with the name 'token'
jwtFromRequest : ExtractJWT.fromUrlQueryParameter('token')
}, async (token, done) => {
try {
//Pass the user details to the next middleware
return done(null, token.user);
} catch (error) {
done(error);
}
}));
Setup protected route, note that you don’t need to manually call jwt.verify, middleware handles it and populates req.user:
const express = require('express');
const router = express.Router();
//Let's say the route below is very sensitive and we want only authorized users to have access
//Displays information tailored according to the logged in user
router.get('/profile', passport.authenticate('jwt', { session: false }), (req, res, next) => {
//We'll just send back the user details and the token
res.json({
message : 'You made it to the secure route',
user : req.user,
token : req.query.token
})
});
module.exports = router;
**Update based on your comment:
I cloned your repo and it is working for me, although I changed some things:
I added
app.use(bodyParser.json()); to app.js so that I could send the request bodies as json - this is not necessary if you prefer urlencoded
the problem is that secureRoute that you export is another router and you try to use it as a controller in app.js:
...
const secureRoute = require('./routes/secure-routes');
...
app.use('/user', passport.authenticate('jwt', { session: false }), secureRoute);`
*note that it will be /user route, if you want /profile please change it in like app.use('/profile', ...)
so instead of
router.get("/profile", (req, res, next) => {
//We'll just send back the user details and the token
res.json({
message: "You made it to the secure route",
user: req.user,
token: req.query.secret_token
});
});
it should be just a controller function:
...
module.exports = (req, res, next) => {
//We'll just send back the user details and the token
res.json({
message: 'You made it to the secure route',
user: req.user,
token: req.query.token // note that you don't use secret_token but token as a name
});
};
the third thing is to not forget to add token to the query params, when you call the API, like http://localhost:3000/user?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7Il9pZCI6IjVlODc2Yjc1YTVlNTk3MTRlOGFjMmI4NyIsImVtYWlsIjoiYUBiLmNvbSJ9LCJpYXQiOjE1ODU5MzMyNjR9.lcLuQeCMRy7Ef9zNkIt_rn4S22t2cm7YLRE7Jgp1Mpw
you should get the response:
{
"message": "You made it to the secure route",
"user": {
"_id": "5e876b75a5e59714e8ac2b87",
"email": "a#b.com"
}
}
I am trying to understand how app.get() works in calling functions when trying to switch between web pages.
I've created a user-login page that assigns a token to the user and checks it in a function.
I use app.post('/login', login); to call the login function which sends the user object to the server. After creating the token I'm hoping to then render the next page in a function after checking the token. (See code below)
However, I don't really understand how app.get('/', checkToken, getProfilePage) is then called. As I don't think it ever gets called.
I've looked at some websites that explain about HTTP requests but I'm struggling to find out, how it all links together inside app.js.
App.js:
app.post('/login', login);
app.get('/', authorize.checkToken, getProfilePage);
function login(req, res, next) {
userService.login(req.body, (err, user) => {
if (err) {
res.redirect('error.ejs');
console.log(error.message);
}
console.log(user);
if (!user) {
res.status(400).json({ success: false, message: 'Username or
password is incorrect' });
}
else {
res.json(user);
}
})
}
The next login function assigns the token and is used above as middleware:
function login({ username, password }, callback) {
grabUsers((err, users) => {
let user = users.find(u => u.username === username && u.password
=== password);
if (user) {
const token = jwt.sign({ username: username }, config.secret,
{ expiresIn: '24h'
}
);
const { password, ...userWithoutPassword } = user;
user = {
...userWithoutPassword,
success: true,
message: 'Authentication successful!',
token: token
}
};
callback(null,user);
})
}
Inside authorize.js:
let jwt = require('jsonwebtoken');
const config = require('./config.js');
let checkToken = (req, res, next) => {
console.log("check token running...");
let token = req.headers['x-access-token'] ||
req.headers['authorization']; // Express headers are auto
converted to lowercase
if (token.startsWith('Bearer ')) {
// Remove Bearer from string
token = token.slice(7, token.length);
}
if (token) {
jwt.verify(token, config.secret, (err, decoded) => {
if (err) {
return res.json({
success: false,
message: 'Token is not valid'
});
} else {
req.decoded = decoded;
next();
}
});
} else {
return res.json({
success: false,
message: 'Auth token is not supplied'
});
}
};
module.exports = {
checkToken: checkToken }
getProfilePage function:
module.exports = {
getProfilePage: (req, res) => {
res.render('profile.ejs');
}
}
So my login form posts to /login and then once it has been verified I would like to check the token and then call getProfilePage. But how do I call the app.get() after the login data has been posted and authenticated?
You don't call a route from your app, what you need to do is redirect to it : res.redirect('/');
I believe there is a problem with how you try to authenticate a user. Seems to me you send a token in authorization header which is a common practice when you accessing API. Not sure how/when you generate the token and set this header though...
Anyway, this approach is good for authorization but not so good for authentication.
After successful /login request you should set the authentication cookie (user session). To simplify, you can just generate a JWT with userId encoded into it and use it as the value for this cookie (let's call it user-session).
Now after that each time user makes a request the cookie will be sent with it and you can decode the userId from this JWT. (Same thing, but now you'll take token from req.cookies['user-session'] instead of req.headers['authorization']).
But how do I call the app.get() after the login data has been posted and authenticated?
You can either navigate to this page from the client right after you receive successful /login response if you're using AJAX (i.e. window.location.replace('/')) or you can do res.redirect('/') instead of res.json(user) on successful login if you submit the HTML form without AJAX.
Redirect forces a browser to immediately make another request to the URL you specify and by that time you'll have user-session cookie set, i.e. you'll be able to retrieve userId and return correct profile page.
i use koa-passport and passport-local
init strategy
passport.use(new LocalStrategy(
(username, password, cb) => {
db.findByUsername(username, (err, user) => {
return cb(null, user);
});
}));
login
authorization.post('/login', function(ctx, next) {
return passport.authenticate('local', function(user,error) {
if (!user) {
ctx.status = 401;
ctx.body = { success: false, ...error }
} else {
ctx.body = { success: true }
ctx.status = 200;
return ctx.login(user)
}
})(ctx, next)
})
After delete cookies, browser still have access to private url and if i login again with same user then response does't contains "Set-Cookie" header with session id. How can i fix it?
that happen because i use koa-proxy.
Auth logic is located on another server and i use koa-proxy for proxy requests. Now i delete koa-proxy and use http-proxy and http-proxy-rules and all ok.
I have build a couple other expressjs applications, but I just can't find how to pass the User Model to the front-end or put it in the req as a parameter.
The app is a one page web app, so the user uses the login form to post to /login:
app.post('/login', require('./app/controllers/user').login);
the controller picks it up and calls the specific module in order to handle the request:
exports.login = function (req,res) {
AccountHandler.login(req,function (response) {
if (response.code < 0) {
req.flash('error', response.msg);
}
else if (response.code > 0) {
req.flash('success', response.msg);
}
req.user = response.user;
res.redirect(response.url);
});
}
and here is the module handling the login and calling the callback by passing the required arguments:
exports.login = function (req,callback) {
process.nextTick(function () {
User.findOne({ 'valid.email': req.body.Email }, function (err, user) {
if (err) {
callback({url: '/#login', msg: "There was an unexpected error!", code: -10});
}
if (!user) {
callback({url: '/#login', msg: "No such email/password combination was found", code: -1});
}
if (user) {
easyPbkdf2.verify(user.valid.salt, user.valid.password, req.body.Password, function (err, valid) {
if (!valid){
callback({url: '/#login', msg: "No such email/password combination was found", code: -1});
}
else{
callback({user: user, url: '/', msg: "acknowledged", code: 10});
}
});
}
});
});
}
In the Controller I am saying req.user = response.user; which doesn't persist, and when the user is redirected to '/' the req.user is still empty. how can I keep this user information to the redirect page?
If my understanding is correct, the res.redirect() call will actually cause the
browser to redirect to the given url, which will result in a new request to
your express server. Since its a new request, the old req object is no longer
relevant.
I think what you want is to store the logged in user's session using express session middleware. Lots of good examples are out there.. Here is one.
So the solution is to use session + middleware logic/functionality.
Here is the setup (exrepssjs):
Using the express-session module:
var sessionMiddleware = session({
resave: true,
saveUninitialized: true,
httpOnly: true,
genid: function(req) {
return uuid.v1() // I used another module to create a uuid
},
secret: 'random secret here',
cookieName: 'session', // if you dont name it here it defaults to connectsid
});
following create a middleware, so that you process the session on every request as follows (simplified, i do a lookup on the user overtime and pass it back to the front end):
app.use(function(req, res, next) {
res.locals.success_messages = req.flash('success_messages');
res.locals.error_messages = req.flash('error_messages');
if (req.session && req.session.user) {
User.findOne({ email: req.session.user.email }, function(err, user) {
if (user) {
req.user = user;
delete req.user.password; // delete the password from the session
req.session.user = user; //refresh the session value
res.locals.user = user;
}
// finishing processing the middleware and run the route
next();
});
} else {
next();
}
});
Now from the front-end most of the time using either ejs or jade you can access the user using the "user" variable.