First of all this is NOT a duplicate since all the documentation/answers out there are for versions prior to v1.0 and they don't seem to work.
I'm trying to implement a simple authentication with passport and SailsJS v1.0.
The problem is...since i'm new to this and sailsjs (v1) seems to lack online examples, i'm pretty stuck.
The app should work like this -> User registers, validates their email and then logs in. Upon logging in the user gets back a accessToken which he needs to use to make requests to protected routes (via Bearer or something else).
The tokens should be saved in the DB so i can invalidate them when the user changes password and such.
How could i achieve something like this ? This is what i've got so far (merging older/newer examples online).
User.js (model)
const bcrypt = require('bcryptjs');
module.exports = {
attributes: {
email: {
type: 'string',
required: true,
unique: true
},
username: {
type: 'string',
required: true,
unique: true
},
password: {
type: 'string',
required: true
},
tokens: {
collection: 'token',
via: 'userId'
}
},
customToJSON: function () {
return _.omit(this, ['password'])
},
beforeCreate: function (user, cb) {
bcrypt.genSalt(10, function (err, salt) {
bcrypt.hash(user.password, salt, null, function (err, hash) {
if (err) return cb(err);
user.password = hash;
return cb();
});
});
}
};
Token.js (model)
module.exports = {
attributes: {
token: {
type: 'string',
required: true,
unique: true
},
userId: {
mode: 'user'
},
isValid: {
type: 'bool',
}
},
};
Since i'm very new to node and especially sails i've got quite a few questions.
How could i create a token on user login if the token in the DB is
invalid (or should i just use 1 token at registration/change
password ?)
How do i implement the actual authentication process
using passport ?
Should the token be a specific format (number of characters etc) for
security reasons ?
Is there a better approachto what i'm trying to achieve ? (to be
more exact i want a REST Api back-end to serve my ReactJS front-end
with the ability to reuse the same back-end for android/ios etc).
Any tips, links, suggestions etc will be highly appreciated. Thanks and have mercy on my lack of knowledge!
EDIT: Would this approach stop me from using social media login (i want to implement that in the future too)?
You can use JWT for authentication.
Create file called isAuthenticated.js in api/policies folder with following code.
const passport = require('passport');
module.exports = async (req, res, proceed) => {
passport.authenticate('jwt', { session: false }, (err, user, info) => {
if (err) {
res.serverError(err, err.message);
}
if (user) {
req.user = user;
return proceed();
}
if (info) {
return res.forbidden(info);
}
// Otherwise, this request did not come from a logged-in user.
return res.forbidden();
})(req, res, proceed);
};
Create strategies folder in api folder with jwt.js file and following code.
const JwtStrategy = require('passport-jwt').Strategy;
const { ExtractJwt } = require('passport-jwt');
const passport = require('passport');
const opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = JWT_SECRET;
opts.issuer = JWT_ISSUER;
opts.audience = JWT_AUDIENCE;
opts.jsonWebTokenOptions = {
expiresIn: JWT_EXPIRES_IN,
};
module.exports = {
/**
* Passport Strategy
*/
passport: () => {
passport.use(new JwtStrategy(opts, (jwtPayload, done) => {
User.findOne({ id: jwtPayload.id }, (err, user) => {
if (err) {
return done(err, null);
}
if (user) {
return done(null, user);
}
return done({ message: 'No user account found' }, 'No user account found');
});
}));
},
};
And in app.js require('./api/strategies/jwt').passport(); at top of the file.
Now you can apply policies to the routes in config/policies.js.
To generate JWT, you can use the following code.
const jwt = require('jsonwebtoken');
module.exports = {
generate: (id, email) => jwt.sign({ id, email }, jwtSecret, {
audience: jwtAudience,
expiresIn: jwtExpiresIn,
issuer: jwtIssuer,
})
};
Related
Edit: [how to handle case of jwt expiration ]
I have read some article on how to implement email verification for your web application and each one follow up:
Creating a unique string, saving it in db with reference to user being verified and sending that unique string as a link for verification. When user visits that link, unique string is run against db and refernced user is validated.
But, I tried it in a different way, that user model contains verify status and will be false by default and when new user sign_up then a jwt token is created and that is sent to user as verification link and when the link is visited, jwt token is verified and user verify status is changed to true.
Above implementation worked for me and removes the use of creating and storing token in separate db but I am afraid this approach might have problems which I might not be aware of. here's the code for above.
passport configuration for auth(config-passport.js)
const bcrypt = require('bcrypt')
const LocalStrategy = require('passport-local').Strategy
const { User } = require('./models/user');
module.exports = (passport) => {
// passport local strategy
const authUser = (email, password, done) => {
User.findOne({ email: email }, function(err, user){
if(err) return done(err);
if(!user || !user.verify) return done(null, false);
if(user.verify){
bcrypt.compare(password, user.password, (err, isValid) => {
if (err) {
return done(err)
}
if (!isValid) {
return done(null, false)
}
return done(null, user)
})
}
})
}
passport.serializeUser((user, done) => {
done(null, user.id)
});
passport.deserializeUser((id, done) => {
User.findOne({ _id: id }, function(err, user){
done(err, user)
});
});
passport.use(new LocalStrategy({
usernameField: 'email'
}, authUser));
}
user model
'use strict';
const mongoose = require('mongoose');
const bcrypt = require('bcrypt')
const Joi = require('joi');
const Schema = mongoose.Schema;
//any changes done to userSchema will need changes done to userValidation.js
const userSchema = new Schema({
username: {type: String, required: true, maxlength: 100},
email: {type: String, unique: true, lowercase: true, required: true},
mobile: {type: Number, unique: true, required: true},
password: {type: String, required: true},
verify: { type: Boolean, enum: [false, true], default: false },
lib: [{ type: Schema.Types.ObjectId, ref: 'Book' }],
book_id: [{ type: Schema.Types.ObjectId, ref: 'Book' }]
});
const JoiValidUser = Joi.object({
username: Joi.string().min(3).max(50).required(),
email: Joi.string().email().min(5).max(50).required(),
mobile: Joi.string().regex(/^[0-9]{10}$/).required().messages({ 'string.pattern.base': `Phone number must have 10 digits.` }),
password: Joi.string().min(5).max(255).required()
});
userSchema.pre('save', async function(next){
const user = this;
const hash = await bcrypt.hash(user.password, 10);
this.password = hash;
next();
})
userSchema.methods.isValidPassword = async function(password) {
const user = this;
const compare = await bcrypt.compare(password, user.password);
return compare;
}
const User = mongoose.model('User', userSchema);
module.exports = { User, JoiValidUser };
user creation controller(userCreate.js)
const { User, JoiValidUser } = require('../models/user');
const mailer = require('../controller/mailHandler')
//takes data posted and form it in a readable format
//then validate/sanitize it against schema
//if error arises or user already exists a msg is passed on
//else user creation process is executed
module.exports = async function(req, res){
let user = {
username: req.body.username,
email: req.body.email,
mobile: req.body.mobile,
password: req.body.password
}
try{
JoiValidUser.validate(user);
const ExistUser = await User.findOne({
$or: [
{ email: req.body.email },
{ mobile: req.body.mobile }
]
});
if(ExistUser)
throw new Error("Email/Mobile Number already Registered");
await (new User(user)).save();
mailer(user.username, user.email);
res.send({ msg: "A Verification link is sent to mail" });
} catch(err) {
res.render('error', { message: err.message })
}
}
user verification route (verify.js)
const router = require('express').Router();
const jwt = require('jsonwebtoken');
const config = require('dotenv').config().parsed
const { User } = require('../models/user')
const routePlan = require('../route_plan');
router.get('/:token', async(req, res) => {
const { email } = jwt.verify(req.params.token, config.SECRET);
await User.findOneAndUpdate({ email: email }, {
$set: { verify: true }
});
res.send("Welcome ...")
})
module.exports = router;
EDIT:
Thank you all for your feedback but there is another problem I want to be clear of on how to handle case when jwt token expires because link will be invalid and user cannot try to sign up again because his info is already in db and he cannot register again
I'm trying to add a role called admin to authenticate admins logged into dashboard web app while the normal user can just access the regular pages.
For a normal user, I require the passport in server.js like
// use passport
app.use(passport.initialize());
require("./config/passport")(passport);
In the config/passport.js, like the code in official example, I try this:
const JwtStrategy = require('passport-jwt').Strategy,
ExtractJwt = require('passport-jwt').ExtractJwt;
const mongoose = require('mongoose');
const User = mongoose.model("users");
const key =require("../config/key");
const opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = key.secretKey;
module.exports = passport => {
passport.use(new JwtStrategy(opts, (jwt_payload, done) => {
// console.log(jwt_payload);
User.findById(jwt_payload.id)
.then(user => {
if(user) {
return done(null, user);
}
return done(null, false);
})
.catch(err => console.log(err));
}));
};
This way works fine, and I use them in the route
router.get("/current", passport.authenticate("jwt", {session: false}), (req, res) => {
res.json({
id: req.user.id,
name: req.user.name,
username: req.user.username,
email: req.user.email,
avatar: req.user.avatar,
});
})
However, while I'm adding a role in the token rule:
const rule = {id:admin.id, email: admin.email, avatar: admin.avatar, admin: admin.admin};
How could I check if the admin property is true to query different Collections in passport.js
I tried this, which doesn't work for me with the error seems like the server run twice:
module.exports = passport => {
passport.use(new JwtStrategy(opts, (jwt_payload, done) => {
// console.log(jwt_payload);
if(jwt_payload.admin){
Admin.findById(jwt_payload.id)
.then(user => {
if(user) {
return done(null, user);
}
return done(null, false);
})
.catch(err => console.log(err));
} else {
User.findById(jwt_payload.id)
.then(user => {
if(user) {
return done(null, user);
}
return done(null, false);
})
.catch(err => console.log(err));
}
}));};
The error is :
Error
Here is what I do and it works pretty well... I simply include the isAdmin: Boolean in my user model like so:
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
minlength: 5,
maxlength: 50
},
email: {
type: String,
required: true,
minlength: 5,
maxlength: 255,
unique: true
},
password: {
type: String,
required: true,
minlength: 5,
maxlength: 1024
},
isAdmin: Boolean
});
and then include this in the jwt like so:
userSchema.methods.generateAuthToken = function() {
const token = jwt.sign({ _id: this._id, isAdmin: this.isAdmin }, config.get('jwtPrivateKey'));
return token;
}
then a custom middleware to check the value of isAdmin like so:
module.exports = function (req, res, next) {
if (!req.user.isAdmin) return res.status(403).send('Access denied.');
next();
}
then I simply import it and use it as the second param for any route like so:
router.patch('/:id', [auth, isAdmin, validateObjectId], async (req, res) => {
// handle the route (in order to do anything in this route you would need be an admin...)
});
EDIT: If you're curious about the other two middleware here they are...
auth.js:
const jwt = require('jsonwebtoken');
const config = require('config');
module.exports = function (req, res, next) {
const token = req.header('x-auth-token');
if (!token) return res.status(401).send('Access denied. No token provided.');
try {
const decoded = jwt.verify(token, config.get('jwtPrivateKey'));
req.user = decoded;
next();
}
catch (ex) {
res.status(400).send('Invalid token.');
}
}
validateObjectId:
const mongoose = require('mongoose');
module.exports = function(req, res, next) {
if (!mongoose.Types.ObjectId.isValid(req.params.id))
return res.status(404).send('Invalid ID.');
next();
}
Here my authentication process:
Session > Controller.js
const jwt = require("jsonwebtoken");
const repository = require("./repository");
const config = require("../../config");
const logger = require("../../utilities/logger");
exports.login = async (req, res) => {
if (!req.body.password || !req.body.username) {
res.preconditionFailed("Credentials required");
return;
}
try {
const user = await repository.findUser(req.body);
if (!user || !user.comparePasswords(req.body.password)) {
res.json({ success: false, message: "Authentication failed." });
return;
}
const token = jwt.sign(user.toObject(), config.secret, { expiresIn: 1440 });
logger.info("User loged in with success. Login token", token);
res.json({
success: true,
token,
});
} catch (err) {
res.send(err);
}
};
User > Model.js
const mongoose = require("mongoose");
const bcrypt = require("bcrypt");
const SALT_WORK_FACTOR = 10;
const Schema = mongoose.Schema;
const userSchema = new Schema({
id: { type: String, required: true },
username: { type: String, required: true },
password: { type: String, required: true },
}, {
timestamps: true,
});
userSchema.pre('save', function(next) {
var user = this;
// only hash the password if it has been modified (or is new)
if (!user.isModified('password')) return next();
// generate a salt
bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
if (err) return next(err);
// hash the password using our new salt
bcrypt.hash(user.password, salt, function(err, hash) {
if (err) return next(err);
// override the cleartext password with the hashed one
user.password = hash;
next();
});
});
});
userSchema.methods.comparePasswords = function(candidatePassword) {
return bcrypt.compareSync(candidatePassword, this.password);
};
module.exports = mongoose.model("User", userSchema);
In the user model, I'm using the bcrypt method compareSync. It's recommended to use the async method compare (https://www.npmjs.com/package/bcrypt).
Could someone explain why? And what would be a good implementation? It's working well like that but I would like to be sure I'm using the best practices.
compareSync is extremely CPU intensive task. Since Node.js Event Loop is single threaded, you block entire application unless you comparison got finished.
Main rule using node.js is to always avoid synchronous code execution.
Reason is event-driven nature of framework.
Good & detailed explanation can be found here
Im trying to setup my Node JS API.
I have a User model :
// Dependencies
var restful = require('node-restful');
var mongoose = restful.mongoose;
var bcrypt = require('bcrypt');
// Schema
var userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true},
firstname: {
type: String,
required: true
},
lastname: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true,
lowercase: true
},
password: {
type: String,
required: true},
},
{
timestamps: true
});
// Saves the user's password hashed
userSchema.pre('save', function (next) {
var user = this;
if (this.isModified('password') || this.isNew) {
bcrypt.genSalt(10, function (err, salt) {
if (err) {
return next(err);
}
bcrypt.hash(user.password, salt, function(err, hash) {
if (err) {
return next(err);
}
user.password = hash;
next();
});
});
} else {
return next();
}
});
// Use bcrypt to compare passwords
userSchema.methods.comparePassword = function(pw, cb) {
bcrypt.compare(pw, this.password, function(err, isMatch) {
if (err) {
return cb(err);
}
cb(null, isMatch);
});
};
module.exports = restful.model('Users', userSchema);
I want to use passport with jwt for authentication :
// Dependencies
var JwtStrategy = require('passport-jwt').Strategy;
var ExtractJwt = require('passport-jwt').ExtractJwt;
var config = require('../config/database');
// Load models
var User = require('../models/user');
// Logique d'authentification JWT
module.exports = function(passport) {
var opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderWithScheme('JWT');
opts.secretOrKey = config.secret;
opts.audience = 'localhost';
passport.use(new JwtStrategy(opts, function(jwt_payload, done) {
User.findById(jwt_payload._id, function(err, user) {
if (err) {
return done(err, false);
}
if (user) {
done(null, user);
} else {
done(null, false);
}
});
}));
passport.use(new JwtStrategy(opts, function(jwt_payload, done) {
Company.findById(jwt_payload._id, function(err, company) {
if (err) {
return done(err, false);
}
if (company) {
done(null, company);
} else {
done(null, false)
}
});
}));
};
And my route for authentication :
// User
router.post('/users/login', (req, res) => {
User.findOne({
email: req.body.email
}, (err, user) => {
if (err) throw err;
if (!user) {
res.json({success: false, message: 'Authentication failed. User not found.'});
} else {
// Check if passwords matches
user.comparePassword(req.body.password, (err, isMatch) => {
if (isMatch && !err) {
// Create token if the password matched and no error was thrown
var token = jwt.sign(user, config.secret, {
expiresIn: 10080 // in seconds
});
res.json({success: true, token: 'JWT ' + token, user: {
id: user._id,
username: user.username,
email: user.email
}});
} else {
res.json({success: false, message: 'Authentication failed. Passwords did not match.'});
}
});
}
});
});
Everything work great on postman.
The token is correctly generated and signed with user's informations.
But i have a problem with the authentication on a protected route :
router.get('/users/profile', passport.authenticate('jwt', { session: false }), function(req, res) {
res.send('It worked! User id is: ' + req.user._id + '.');
});
Everytime, it gives me an "Unauthorized 401" Error.
I really dont know where is the problem, i think the problem is around jwtFromRequest, i also tried with Bearer but it also doesn't work...
I think a good option to avoid this kind of problems is to start from a base project that uses this authentication strategy, and after you have that working, modify it with your functionality.
Here you have an example with jwt authentication strategy and Refresh token implementation: https://solidgeargroup.com/refresh-token-autenticacion-jwt-implementacion-nodejs?lang=es
I am attempting to use Passport Authentication on my web app. I am using Sequelize ORM, Reactjs front-end and express and node back end. Right now, when I register a user everything works fine. the problem comes when I try to login. I see the user querying the DB to find the user with correct email, but when it is time to compare passwords, i am catching an error.
"Unhandled rejection TypeError: dbUser.validPassword is not a function"
here is my config/passport.js file:
var passport = require("passport");
var LocalStrategy = require("passport-local").Strategy;
var db = require("../models");
// Telling passport we want to use a Local Strategy. In other words, we
want login with a username/email and password
passport.use(new LocalStrategy(
// Our user will sign in using an email, rather than a "username"
{
usernameField: "email"
},
function(email, password, done) {
// When a user tries to sign in this code runs
db.User.findOne({
where: {
email: email
}
}).then(function(dbUser) {
// If there's no user with the given email
if (!dbUser) {
return done(null, false, {
message: "Incorrect email."
});
}
// If there is a user with the given email, but the password the user
gives us is incorrect
else if (!dbUser.validPassword(password)) {
return done(null, false, {
message: "Incorrect password."
});
}
// If none of the above, return the user
return done(null, dbUser);
});
}
));
// In order to help keep authentication state across HTTP requests,
// Sequelize needs to serialize and deserialize the user
// Just consider this part boilerplate needed to make it all work
passport.serializeUser(function(user, cb) {
cb(null, user);
});
passport.deserializeUser(function(obj, cb) {
cb(null, obj);
});
// Exporting our configured passport
module.exports = passport;
Here is my User Model:
var bcrypt = require("bcrypt-nodejs");
[![enter image description here][1]][1]module.exports = function(sequelize, DataTypes){
var User = sequelize.define("User", {
email: {
type: DataTypes.STRING,
allowNull: false,
validate: {
isEmail: true
}
},
password: {
type: DataTypes.STRING,
allowNull: false
},
},{
classMethods: {
associate: function(models) {
User.hasOne(models.Educator, {
onDelete: "cascade"
});
User.hasOne(models.Expert, {
onDelete: "cascade"
});
}
},
instanceMethods: {
validPassword: function(password) {
return bcrypt.compareSync(password, this.password);
}
},
// Hooks are automatic methods that run during various phases of the User Model lifecycle
// In this case, before a User is created, we will automatically hash their password
hooks: {
beforeCreate: function(user, options) {
console.log(user, options )
user.password = bcrypt.hashSync(user.password, bcrypt.genSaltSync(10), null);
}
}
})
return User;
}
I am also including an image of the error.
As of sequelize version >4, it has changed the way instance methods are defined.
They follow a more class based approach now,
A sample from the Docs for how it has to be done
const Model = sequelize.define('Model', { ... });
// Class Method
Model.associate = function (models) { ...associate the models };
// Instance Method
Model.prototype.someMethod = function () {..}
The syntax you are using corresponds to sequelize < 4.