Implement PassportJS (local-strategy) custom callback using async/await - node.js

I've implemented passport local strategy using async/await as below
const strategy = new LocalStrategy(
async(username, password, done) => {
try {
// Find the user given the username
const user = await User.findOne({ username });
// If not, send info
if (!user) {
return done(null, false, {
success: false,
message: 'User not found'
})
}
// Check if the password is correct
const isMatch = await user.isValidPassword(password);
// If not, send info
if (!isMatch) {
return done(null, false, {
success: false,
message: 'Invalid Password'
});
}
// Otherwise, return the user
done(null, user);
} catch (error) {
done(error, false);
}
}
);
passport.use(strategy);
And implemented custom callback in routes using the code below.
router.post('/login', async(req, res, next) => {
const { receivedUser, information } = await passport.authenticate('local');
// If a user is found
if (receivedUser) {
res.status(200).json({
success: true,
message: 'Authentication Successful'
});
} else {
// If user is not found
res.status(401).json(information);
}
};
);
There are errors in above custom callback implementation as receivedUser and information are 'undefined'. How to make changes to above custom callback using async/await to remove errors ?
Referred docs:
http://passportjs.org/docs/configure
http://passportjs.org/docs/username-password

Related

Why Passport.js is correctly redirecting after a successful login?

Im trying to implement a Local Strategy using NodeJs/Express/Passport.js. I already have a google oauth that is working perflecty. But the Local strategy does not work, I can see that the user is found but it won't ever redirect to the success url.
This is my auth route the goole one is the exact same thing but it work.
router.post("/login", passport.authenticate('local', {
successRedirect: successLoginUrl,
failureRedirect: errorLoginUrl,
failureMessage: true
}));
My passport.ts
passport.use(new LocalStrategy({ passReqToCallback: true },
async (req: any, username: string, password: string, done: done) => {
try {
//We find the user corresponding to the username
const user = await User.findOne(username) as Array<User>;
if (user.length == 0) {
//User does not exist...
return done(null, false, { message: "Email or Password is not correct" });
}
//We check if the password is OK
const match = await bcrypt.compare(password, user[0].password);
if (match) {
req.user = user[0];
return done(null, user[0])
} else {
return done(null, false, { message: "Email or Password is not correct" })
}
} catch (error) {
console.log("ERR", error);
return done(error);
}
})
);
My user serialization/deserialize
passport.serializeUser((user, done) => {
done(null, (user as User).id)
})
passport.deserializeUser(async (id, done) => {
const user = await User.findOne(id as number).catch((err: Error) => {
done(err, null);
})
if (user) done(null, user as Iusers);
})
Iv'e noticed that deserializeUser is never called but serialize yes.
Thanks to anyone !

Token invalid: JsonWebTokenError: jwt malformed nodejs

I am learning how to use NodeJS, and while following an online tutorial I incurred in the error in the title.
I am using the latest version of NodeJS, and the jsonwebtoken authentication.
This is how I generate the token:
router.post('/login', (req, res) => {
// Check if username was provided
if (!req.body.username) {
res.json({
success: false,
message: 'No username was provided'
}); // Return error
} else {
// Check if password was provided
if (!req.body.password) {
res.json({
success: false,
message: 'No password was provided.'
}); // Return error
} else {
// Check if username exists in database
User.findOne({
username: req.body.username.toLowerCase()
}, (err, user) => {
// Check if error was found
if (err) {
res.json({
success: false,
message: err
}); // Return error
} else {
// Check if username was found
if (!user) {
res.json({
success: false,
message: 'Username not found.'
}); // Return error
} else {
const validPassword = User.comparePassword(req.body.password, user.password); // Compare password provided to password in database
// Check if password is a match
if (!validPassword) {
res.json({
success: false,
message: 'Password invalid'
}); // Return error
} else {
const token = jwt.sign({
userId: user._id
}, 'goodsecret', {
expiresIn: '24h'
}); // Create a token for client
res.json({
success: true,
message: 'Success!',
token: token,
user: {
username: user.username
}
}); // Return success and token to frontend
}
}
}
});
}
}
});
And here how I evaluate it:
const jwt = require('jsonwebtoken');
/* ================================================
MIDDLEWARE - Used to grab user's token from headers
================================================ */
router.use((req, res, next) => {
const token = req.headers.authorization; // Create token found in headers
// Check if token was found in headers
if (!token) {
res.json({
success: false,
message: 'No token provided'
}); // Return error
} else {
// Verify the token is valid
console.log(token);
jwt.verify(token, 'goodsecret', (err, decoded) => {
// Check if error is expired or invalid
if (err) {
res.json({
success: false,
message: 'Token invalid: ' + err
}); // Return error for token validation
} else {
req.decoded = decoded; // Create global variable to use in any request beyond
next(); // Exit middleware
}
});
}
});
/* ===============================================================
Route to get user's profile data
=============================================================== */
router.get('/profile', (req, res) => {
// Search for user in database
User.findOne({
_id: req.decoded.userId
}).select('username email').exec((err, user) => {
// Check if error connecting
if (err) {
res.json({
success: false,
message: err
}); // Return error
} else {
// Check if user was found in database
if (!user) {
res.json({
success: false,
message: 'User not found'
}); // Return error, user was not found in db
} else {
res.json({
success: true,
user: user
}); // Return success, send user object to frontend for profile
}
}
});
});
Whenever I try to generate a request from HttpRequester, sending the token, I get the error in the title. The token I am sending is
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1OWRlMTYzNTMwOWY2NDM1YjBjOWRmM2UiLCJpYXQiOjE1MDc3NDMzODYsImV4cCI6MTUwNzgyOTc4Nn0.uJwpYN7IHYg_lmCVCpFg-zfo0QVPglEvWHs7SD9cPkg
and on https://jwt.io/ it works fine, using the secret 'goodsecret' as in the code. What am I missing?
This is a screen on how I am generating requests.
Thank you for your help

passport local strategy's not getting called

I'm currently making app with vue & express.
I adopted passport as authentication library and I've used it several times.
so I know that when I make login request, passport middleware authenticates user by 'xx strategy'.
The problem is register request works well but login isn't.
I constantly get 'false' for response.data.
I can't even guess where the false comes from.
There is no return statements that returns false.
follow is my code.
passport.deserializeUser((authId, done) => {
let hasUser
let users = User.findOneByAuthId(authId)
.then(results => (hasUser = results.length))
if (hasUser) {
console.log('deserialize!')
done(null, users[0])
} else {
console.log('no user')
done(null, { message: 'no user' })
}
})
passport.use(new LocalStrategy(
(username, password, done) => {
let hasUser
let users = User.findOneByEmail(username)
.then(results => (hasUser = results.length))
if (hasUser) {
let user = users[0]
let { hash } = user
if (User.checkPassword(password, hash)) {
done(null, { message: 'success' })
} else {
done(null, { message: 'wrong password' })
}
} else {
done(null, { message: 'wrong email' })
}
}
))
router.post('/register', (req, res) => {
if (!fun.emailChecker(req.body.username)) {
return res.status(403).json({
message: 'Invalid Email'
})
}
if (!fun.passwordChecker(req.body.password)) {
return res.status(403).json({
message: 'Invalid Password'
})
}
let hasUser
User.findOneByEmail(req.body.username)
.then(results => (hasUser = results.length))
if (hasUser) {
return res.status(409).json({
message: 'Email Exist'
})
} else {
let user = {
authId: 'local: ' + req.body.username,
email: req.body.username,
hash: User.hashPassword(req.body.password),
displayName: req.body.displayName
}
User.create(user)
.then(results => {
if (!results) {
throw new Error('user creation error')
} else {
req.login(user, err => {
if (!err) {
req.session.save(() => {
return res.json({ success: true })
})
}
})
}
})
}
})
router.post('/login', (req, res) => {
passport.authenticate('local', (err, result) => {
if (!err) {
return res.json(result)
}
})(req, res)
})
// vue component
methods: {
onSubmit () {
axios.post('http://localhost:3001/auth/login', {
email: this.email,
password: this.password
}).then(response => console.log(response.data))
},
There are various issues with your code.
Starting with incorrect promise handling:
let hasUser
let users = User.findOneByAuthId(authId)
.then(results => (hasUser = results.length))
if (hasUser) { ... }
You are trying to make asynchronous code synchronous here. The code that depends on the result of the asynchronous query has to be moved to inside the then handler (both in deserializeUser and the strategy verification handler):
User.findOneByAuthId(authId).then(users => {
let hasUser = users.length;
if (hasUser) {
console.log('deserialize!')
done(null, users[0]);
} else {
console.log('no user')
done(Error('no user'));
}
});
(I'm not quite sure why your method findOneByAuthId, whose name implies that there will be at most one result, could result in an array)
Also, you're not using the correct convention for passing user data and login errors back to Passport:
if (User.checkPassword(password, hash)) {
done(null, { message: 'success' })
} else {
done(null, { message: 'wrong password' })
}
This should look like this (and obviously, other places where done is called incorrectly should be fixed too):
if (User.checkPassword(password, hash)) {
done(null, user);
} else {
done(null, false, { message: 'wrong password' })
}
(documented here under "Verify Callback")
Finally, you're using passport.authenticate() incorrectly, which is probably the cause of the false being returned. Instead, try this:
router.post('/login', passport.authenticate('local'), (req, res) => {
return res.json(req.user); // or whatever you want to return in case of login success
})
(documented here; if you want to use a custom callback, look for "Custom Callback" on that page)

Using async/await with done()/next() middleware functions

I'm starting to use async/await. Generally, what is a pattern to use await with middleware done/next functions?
For example, how could I replace .then() in the code below with await? localAuthenticate is done/next middleware. Do I need to make a separate async function to use await inside it?
I'd like something like this (even better w/o the try/catch):
function localAuthenticate(User, email, password, hostname, done) {
try { // where is async?
// Find user
let user = await User.findOne({ email: email.toLowerCase() }).exec()
if(!user) return done(null, false, { message: 'This email is not registered.' });
// Test password
user.authenticate(password, function(authError, authenticated) {
if(authError) return done(authError);
if(!authenticated) return done(null, false, { message: 'This password is not correct.' });
return done(null, user);
});
} catch(err) { done(err); }
}
Original code from Passport.js authentication middleware:
function localAuthenticate(User, email, password, hostname, done) {
User.findOne({
email: email.toLowerCase()
}).exec()
.then(user => {
if(!user) {
return done(null, false, {
message: 'This email is not registered.'
});
}
user.authenticate(password, function(authError, authenticated) {
if(authError) {
return done(authError);
}
if(!authenticated) {
return done(null, false, { message: 'This password is not correct.' });
} else {
return done(null, user);
}
});
})
.catch(err => done(err));
}
await can only be called within an async function - see the MDN documentation
Your function needs to be async function localAuthenticate(User, email, password, hostname, done).
The try/catch is the way to catch exceptions when using await, instead of the .then/.catch you are used to when dealing with Promises directly.
Your function would approximate, when using async/await:
async function localAuthenticate(User, email, password, hostname, done) {
try {
// Find user
let user = await User.findOne({ email: email.toLowerCase() }).exec()
if (!user) {
return done(null, false, { message: 'This email is not registered.' })
}
user.authenticate(password, function (authError, authenticated) {
if (authError) {
return done(authError)
}
if (!authenticated) {
return done(null, false, { message: 'This password is not correct.' });
}
return done(null, user);
})
} catch (err) {
done(err)
}
}
Further reading:
http://rossboucher.com/await/#/
https://ponyfoo.com/articles/understanding-javascript-async-await

How to show custom error messages using passport and express

I check if the email already exist when registering a user. If a user already exist I pass the error message "email already exists". But in the front end it shows "Unauthorized" error 401. I want to pass the error message which I pass to the front end from the back end but it pass the default message. below is how I check if a user already exist and send the error message,
exports.register = new LocalStrategy({
usernameField: 'email',
passReqToCallback: true
}, function(req, email, password, done,res) {
var newUser = new User({
email: email,
password: password,
name: req.body.fname
});
var searchUser = {
email: email
};
User.findOne(searchUser, function(err, user) {
if (err) return done(err);
console.log("err "+err);
if (user) {
return done(null, false, {
message: "email already exists"
});
}
newUser.save(function(err) {
console.log("newUser "+newUser);
done(null, newUser);
})
});
});
I use passport for authentication,
authRouter.post('/signup', passport.authenticate('local-register'),function(req, res) {
createSendToken(req.user, res);
});
The error message is not the one I pass to the front end. It shows unauthorized error, which is the default error. When I print the error message in the console in front end it shows,
Object {data: "Unauthorized", status: 401, config: Object, statusText: "Unauthorized"}
You are not saying what output you want in your front end, but I'm guessing you want to have data to be the message that you set in your LocalStrategy.
Here is one way to do that:
authRouter.post('/signup', function(req, res, next) {
passport.authenticate('local-register', function(err, user, info) {
if (err) { return next(err); }
if (!user) {
res.status(401);
res.end(info.message);
return;
}
createSendToken(req.user, res);
})(req, res, next);
});
You can take advantage of passReqToCallback: true in passport.
With this option enabled, req will be passed as the first argument to the verify callback.
Also by taking advantage of flash messages.
Here is basic example of how you use it,
// your strategy
passport.use(new LocalStrategy({
passReqToCallback: true
},
(req, username, password, done) => {
User.findOne({ username: username }, (err, user) => {
if (err) done(err)
if (!user) {
console.log('user does not exists')
return done(null, false, req.flash('message', 'User does not exist' ))
} else {
console.log('user exists')
return done(null, user, req.flash('message', 'User exist'))
}
})
}
))
// your GET login
router.get("/login", (req, res) => {
var message = req.flash('message')
res.render("pages/login", { message })
})
As mentioned in other answers you can manually check the info parameter and then return your own message based on that:
// ❌ MESSY ❌
authRouter.post('/signup', function(req, res, next) {
passport.authenticate('local-register', function(err, user, info) {
if(info.name === 'UserExistsError' ) {
return done(null, false, {
message: "Email already exists"
});
} else if (info.name === 'IncorrectUsernameError') {
return done(null, false, {
message: "Email does not exist"
});
} else if(....
But A MUCH cleaner way is to just specify custom error messages when you create the Account Mongoose model:
var Account = new Schema({
...
});
var options = {
errorMessages: {
UserExistsError: 'Email already exists',
IncorrectUsernameError: 'Email does not exist',
IncorrectPasswordError: ...
}
};
Account.plugin(passportLocalMongoose, options);
Then in your signup route you can simply return the info.message to the user.
// ✨ CLEAN ✨
authRouter.post('/signup', function(req, res, next) {
passport.authenticate('local-register', function(err, user, info) {
return done(null, false, {
message: info.message
});
});
});
If you are using passReqToCallback = true then you can send it just like
if (!isMatch) { return done({ message: 'Incorrect password' }); }

Resources