I'm working on a simple node.js project that requires authentication. I decided to use connect-redis for sessions and a redis-backed database to store user login data.
Here is what I have setup so far:
// Module Dependencies
var express = require('express');
var redis = require('redis');
var client = redis.createClient();
var RedisStore = require('connect-redis')(express);
var crypto = require('crypto');
var app = module.exports = express.createServer();
// Configuration
app.configure(function(){
app.set('views', __dirname + '/views');
app.set('view engine', 'jade');
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.cookieParser());
app.use(express.session({ secret: 'obqc487yusyfcbjgahkwfet73asdlkfyuga9r3a4', store: new RedisStore }));
app.use(require('stylus').middleware({ src: __dirname + '/public' }));
app.use(app.router);
app.use(express.static(__dirname + '/public'));
});
app.configure('development', function(){
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});
app.configure('production', function(){
app.use(express.errorHandler());
});
// Message Helper
app.dynamicHelpers({
// Index Alerts
indexMessage: function(req){
var msg = req.sessionStore.indexMessage;
if (msg) return '<p class="message">' + msg + '</p>';
},
// Login Alerts
loginMessage: function(req){
var err = req.sessionStore.loginError;
var msg = req.sessionStore.loginSuccess;
delete req.sessionStore.loginError;
delete req.sessionStore.loginSuccess;
if (err) return '<p class="error">' + err + '</p>';
if (msg) return '<p class="success">' + msg + '</p>';
},
// Register Alerts
registerMessage: function(req){
var err = req.sessionStore.registerError;
var msg = req.sessionStore.registerSuccess;
delete req.sessionStore.registerError;
delete req.sessionStore.registerSuccess;
if (err) return '<p class="error">' + err + '</p>';
if (msg) return '<p class="success">' + msg + '</p>';
},
// Session Access
sessionStore: function(req, res){
return req.sessionStore;
}
});
// Salt Generator
function generateSalt(){
var text = "";
var possible= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!##$%^&*"
for(var i = 0; i < 40; i++)
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
}
// Generate Hash
function hash(msg, key){
return crypto.createHmac('sha256', key).update(msg).digest('hex');
}
// Authenticate
function authenticate(username, pass, fn){
client.get('username:' + username + ':uid', function(err, reply){
var uid = reply;
client.get('uid:' + uid + ':pass', function(err, reply){
var storedPass = reply;
client.get('uid:' + uid + ':salt', function(err, reply){
var storedSalt = reply;
if (uid == null){
return fn(new Error('cannot find user'));
}
if (storedPass == hash(pass, storedSalt)){
client.get('uid:' + uid + ':name', function(err, reply){
var name = reply;
client.get('uid:' + uid + ':username', function(err, reply){
var username = reply;
var user = {
name: name,
username: username
}
return fn(null, user);
});
});
}
});
});
});
fn(new Error('invalid password'));
}
function restrict(req, res, next){
if (req.sessionStore.user) {
next();
} else {
req.sessionStore.loginError = 'Access denied!';
res.redirect('/login');
}
}
function accessLogger(req, res, next) {
console.log('/restricted accessed by %s', req.sessionStore.user.username);
next();
}
// Routes
app.get('/', function(req, res){
res.render('index', {
title: 'TileTabs'
});
});
app.get('/restricted', restrict, accessLogger, function(req, res){
res.render('restricted', {
title: 'Restricted Section'
});
});
app.get('/logout', function(req, res){
console.log(req.sessionStore.user.username + ' has logged out.');
req.sessionStore.destroy(function(){
res.redirect('home');
});
});
app.get('/login', function(req, res){
res.render('login', {
title: 'TileTabs Login'
});
});
app.post('/login', function(req, res){
authenticate(req.body.username, req.body.password, function(err, user){
if (user) {
req.session.regenerate(function(){
req.sessionStore.user = user;
req.sessionStore.indexMessage = 'Authenticated as ' + req.sessionStore.user.name + '. Click to logout. ' + ' You may now access the restricted section.';
res.redirect('home');
console.log(req.sessionStore.user.username + ' logged in!');
});
} else {
req.sessionStore.loginError = 'Authentication failed, please check your '
+ ' username and password.';
res.redirect('back');
}
});
});
app.get('/register', function(req, res){
res.render('register', {
title: 'TileTabs Register'
});
});
app.post('/register', function(req, res){
var name = req.body.name;
var username = req.body.username;
var password = req.body.password;
var salt = generateSalt();
client.get('username:' + username + ':uid', function(err, reply){
if (reply !== null){
console.log(reply);
req.sessionStore.registerError = 'Registration failed, ' + username + ' already taken.';
res.redirect('back');
}
else{
client.incr('global:nextUserId');
client.get('global:nextUserId', function(err, reply){
client.set('username:' + username + ':uid', reply);
client.set('uid:' + reply + ':name', name);
client.set('uid:' + reply + ':username', username);
client.set('uid:' + reply + ':salt', salt);
client.set('uid:' + reply + ':pass', hash(password, salt));
});
req.sessionStore.loginSuccess = 'Thanks for registering! Try logging in!';
console.log(username + ' has registered!');
res.redirect('/login');
}
});
});
// Only listen on $ node app.js
if (!module.parent) {
app.listen(80);
console.log("Express server listening on port %d", app.address().port);
}
Registration works great. However, upon logging in with the correct user credentials, I am thrown the following error:
node.js:134
throw e; // process.nextTick error, or 'error' event on first tick
^
Error: Can't set headers after they are sent.
I have managed to identify the line that throws this error (res.redirect('home');) in app.post('/login'). Just wondering, besides my poorly written code, what I need to do to fix this error.
UPDATE:
Versions:
node 0.4.10
express 2.4.3
npm 1.0.22
redis 2.4.0 rc5
connect 1.6.0
connect-redis 1.0.6
Here is the link to my app:
http://dl.dropbox.com/u/4873115/TileTabs.zip
Update
The problem was authenticate(). Below I have I think correct implementation:
function authenticate(username, pass, fn){
client.get('username:' + username + ':uid', function (err, reply) {
var uid = reply;
client.get('uid:' + uid + ':pass', function(err, reply){
var storedPass = reply;
client.get('uid:' + uid + ':salt', function(err, reply){
var storedSalt = reply;
if (uid == null){
fn(new Error('cannot find user'));
return;
} else if (storedPass == hash(pass, storedSalt)) {
client.get('uid:' + uid + ':name', function(err, reply){
var name = reply;
client.get('uid:' + uid + ':username', function(err, reply){
var username = reply;
var user = {
name: name,
username: username
}
fn(null, user);
return;
});
});
} else {
return fn(new Error('invalid password'));
}
});
});
});
//return fn(new Error('invalid password'));
}
I can't run the example because I don't have your stylus files. Can't you archive your project and post it over here, so that we can also run your code. If my memory serves me right, you could have these problems when you combine old modules with new modules. Which versions of express, connect-redis, redis, connect, etc do you have installed??
P.S: I can not run your code immediately if you upload, because I have to go to bed and have to work in the morning. But hopefully somebody else can help you then. Or maybe it is matter of modules installed.
My guess is that req.sessionStore.destroy is probably sending a "Set-Cookie" header to expire/delete the session cookie and because there's IO involved, node has a chance to send the HTTP response header before your res.redirect code runs, and thus the error is generated. Try just doing your res.redirect directly inside app.post as opposed to inside the destroy callback and see if that avoids the error.
You may also be hitting this node.js bug if code is trying to read headers after they are sent.
Related
I'm making a chat application with socket.io. I already have a login system where a user is stored in a SQL database.
So I want my chat app to get the user from the database to access the chat.
I have tried several methods, and I often get this error:
Error: SQLITE_CONSTRAINT: NOT NULL constraint failed: messages.username
It should not be allowed be NULL in the username, as I want the users that are logged in to access the chat.
Here is some of my code:
From my server:
io.on('connection', (socket) => {
console.log("a user connected");
});
socket.on('join', async function(name){
socket.username = await getUserByUsername();
io.sockets.emit("addChatter", name);
const messages = await getAllMessages();
io.sockets.emit('messages', messages);
io.sockets.emit('new_message', {username: 'Server', message: 'Velkommen ' + name + '!'});
});
// When server recieves a new message
socket.on('new_message', function(message){
addMessageToDatabase({message: message, username: socket.username});
const username = socket.username
console.log(username + ': ' + message);
io.sockets.emit("new_message", {username, message});
});
// When a user dis
socket.on('disconnect', function(name){
console.log('a user disconnected');
io.sockets.emit("removeChatter", socket.username);
});
});
My client:
const socket = io()
socket.on('connection', async function (e) {
e.preventDefault();
var username = await getUserByUsername();
socket.emit('join', username);
})
// Listens for form submission
$("#chatForm").on('submit', function(e){
e.preventDefault();
var message = $("#message").val();
socket.emit('new_message', message)
$("#message").val("");
})
// adds HTML message to chat
const addMessageToChat = (message) => {
const messageElement = document.createElement('li');
messageElement.innerText = new Date(message.timestamp).toLocaleTimeString('DK')
+ ': ' + message.username
+ ": " + message.message
$("#messagesContainer").append(messageElement);
}
// On receiving one message
socket.on('new_message', function(message){
console.log('message: ', message)
addMessageToChat(message);
})
// on receiving a list of messages
socket.on('messages', function(messages) {
console.log('messages: ', messages)
messages.forEach(message => {
addMessageToChat(message);
})
})
// On person joined chat
socket.on('addChatter', function(name){
var $chatter = $("<li>", {
text: name,
attr: {
'data-name':name
}
})
$("#chatters").append($chatter)
})
// On person disconnect
socket.on("removeChatter", function(name){
$("#chatters li[data-name=" + name +"]").remove()
})
This is PUT method I want to hash my password(using passport) & and update it.
router.put('/reset/:token', function(req, res) {
console.log('listening');
User.findOneAndUpdate({resetPasswordToken:req.params.token},{
password: req.body.password,
resetPasswordToken: undefined,
resetPasswordExpires: undefined
},function(err,user) {
if(err) {
console.log(err + 'is here');
} else {
res.json(user);
}
});
});
I want to hast the variable only password.How can I hash within this method & then update it.
I'm assuming you are using Mongoose. First, create a pre method inside your Schema.
UserSchema
const mongoose = require('mongoose')
, bcrypt = require('bcrypt-nodejs')
, SALT_WORK_FACTOR = 10;
const UserSchema = new mongoose.Schema({
... // schema here
});
/**
* Hash password with blowfish algorithm (bcrypt) before saving it in to the database
*/
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();
user.password = bcrypt.hashSync(user.password, bcrypt.genSaltSync(SALT_WORK_FACTOR), null);
next();
});
mongoose.model('User', UserSchema);
And then in your route:
router.put('/reset/:token', function(req, res, next) {
User.findOne({resetPasswordToken: new RegExp('^' + req.params.token + '$', "i")}, function (err, user) {
if (err)
return next(err);
if (!user)
return res.status(422).json({errors: [{msg: 'invalid reset token'}]});
user.resetPasswordToken = '';
user.resetPasswordExpires = '';
user.password = req.body.password;
user.save().then(function (user) {
return res.status(200).json(user);
});
});
});
I'm trying to develop a small API using express. Just want to have 2 views, which, in my case, means 2 html files. One accesing as default with "/" and the other with "/lessons", so we got 2 get controls plus another one which handles every other get input.
*Both files are in the "views" folder and their extension is: *.html
I have no problem accessing the "app.get("/lessons", function..." in fact I know I can acces to that because a simple "console.log(..)" command. The problem is that I got the next error when trying to render:
[TypeError: this.engine is not a function].
Could you help me? I can't understand where is the problem or what I'm doing wrong. I believe it's in the rendering function and has something to do with its configuration and the lessons.html file because index.html has no problem using the same approach.
var express = require('express');
var app = express();
var mod = require('./module');
app.use(express.static('public'));
app.use(express.static('views'));
var port = process.env.PORT || 8080;
app.listen(port, function() {
console.log('Node.js listening on port ' + port);
});
app.get("/", function(req, res) {
console.log("Passed through /");
res.render('index.html');
});
app.get("/lessons", function(req, res) {
console.log("passed through lessons");
res.render('lessons.html', function(err, html) {
if(err) {
console.log(err);
}
res.send(html);
});
//I have tried to to use just: res.render('lessons.html');
});
app.get("*", function(req, res) {
var usageReq = false;
var urlPassed = req.url;
urlPassed = urlPassed.substring(1, urlPassed.length); //remove first "/"
var expected = mod.seeIfExpected(urlPassed); //returns url(if url) or num(if number) or na(if it doesn't match any)
mod.processInfo(expected, urlPassed, function(answer) {
if (answer.found == false && answer.jsonRes == true && answer.info != "inserted") {
res.json({
"error": answer.info
});
} else {
if (answer.jsonRes == true) {
res.json({
"long_url": answer.url,
"short_url": answer.id
});
} else { // go to url
var newUrl = "https://" + answer.url;
res.redirect(newUrl);
}
}
});
});
Here is the typical code snippet to authenticate with JWT:
var express = require('express');
var bodyParser = require('body-parser');
var jwt = require('jsonwebtoken');
var expressJwt = require('express-jwt');
var app = express();
var secret = 'top secrect';
var jwtOptions = {algorithm: 'HS256', expiresInMinutes: 1};
// We are going to protect /api routes with JWT
app.use('/api', expressJwt({secret: secret}));
//app.use(express.json());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded());
app.use('/', express.static(__dirname + '/'));
app.use(function(err, req, res, next) {
if (err.constructor.name === 'UnauthorizedError') {
console.log(err);
res.send(401, 'Unauthorized');
}
});
app.post('/authenticate', function(req, res) {
//TODO validate req.body.username and req.body.password
//if is invalid, return 401
if (!(req.body.username === 'john.doe' && req.body.password === 'foobar')) {
res.send(401, 'Wrong user or password');
return;
}
// user object (session data) handled by express-jwt
var user = {
session: {
counter: 0
},
first_name: 'John',
last_name: 'Doe',
email: 'john#doe.com',
roles: [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000],
id: 123
};
// We are sending the user inside the token
var token = jwt.sign(user, secret, jwtOptions);
res.json({token: token});
});
app.get('/api/restricted', function(req, res) {
console.log('user ' + req.user.email + ' is calling /api/restricted with roles: ' + req.user.roles);
var token = '';
if (req.headers && req.headers.authorization) {
var parts = req.headers.authorization.split(' ');
if (parts.length === 2) {
var scheme = parts[0]
, credentials = parts[1];
if (/^Bearer$/i.test(scheme)) {
token = credentials;
}
} else {
return new UnauthorizedError('credentials_bad_format', {message: 'Format is Authorization: Bearer [token]'});
}
} else {
return new UnauthorizedError('credentials_required', {message: 'No Authorization header was found'});
}
// verify token: send by client in Authorization HTTP header
// 'session timeout' handled by express-jwt (exp value) and throws 401
jwt.verify(token, secret, jwtOptions, function(err, decoded) {
if (err)
return new UnauthorizedError('invalid_token', err);
req.user = decoded;
console.log(req.user);
});
// update sample data in the session ...
req.user.session.counter = req.user.session.counter + 10;
// ... and create new token ...
var newToken = jwt.sign(req.user, secret, jwtOptions);
// ... and update in the response HTTP header
res.header('Authorization', 'Bearer ' + newToken)
res.json(req.user);
});
app.listen(8080, function() {
console.log('listening on http://localhost:8080');
});
I am wondering why bother to create a handler for '/api/resctricted'? Isn't already been protected by app.use('/', express.static(__dirname + '/'));?
UPDATE:
I've also look into the source code of express-jwt, it looks like it's using jsonwebtoken.verify() to validate the token in the request, this make me feel confused why to use jsonwebtoken.verify() in the '/api/restricted' handler?
I'm trying to build authentication system with ExpressJS and PassportJS. For session store I use Redis. I wanna use Remember Me. Every time when the user signs in and has marked "remember me" check-box, it should automatically sign in by next visit on site. I have downloaded an example app form Github https://github.com/jaredhanson/passport-remember-me and change for my using.
var express = require('express')
, passport = require('passport')
, LocalStrategy = require('passport-local').Strategy
, mongodb = require('mongodb')
, mongoose = require('mongoose')
, bcrypt = require('bcrypt')
, SALT_WORK_FACTOR = 10
, RedisStore = require('connect-redis')(express);
mongoose.connect('localhost', 'test');
var db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', function callback() {
console.log('Connected to DB');
});
// User Schema
var userSchema = mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true},
accessToken: { type: String } // Used for Remember Me
});
// Bcrypt middleware
userSchema.pre('save', function(next) {
var user = this;
if(!user.isModified('password')) return next();
bcrypt.genSalt(SALT_WORK_FACTOR, 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();
});
});
});
// Password verification
userSchema.methods.comparePassword = function(candidatePassword, cb) {
bcrypt.compare(candidatePassword, this.password, function(err, isMatch) {
if(err) return cb(err);
cb(null, isMatch);
});
};
// Remember Me implementation helper method
userSchema.methods.generateRandomToken = function () {
var user = this,
chars = "_!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890",
token = new Date().getTime() + '_';
for ( var x = 0; x < 16; x++ ) {
var i = Math.floor( Math.random() * 62 );
token += chars.charAt( i );
}
return token;
};
// Seed a user
var User = mongoose.model('User', userSchema);
var usr = new User({ username: 'bob', email: 'bob#example.com', password: 'secret' });
usr.save(function(err) {
if(err) {
console.log(err);
} else {
console.log('user: ' + usr.username + " saved.");
}
});
// Passport session setup.
// To support persistent login sessions, Passport needs to be able to
// serialize users into and deserialize users out of the session. Typically,
// this will be as simple as storing the user ID when serializing, and finding
// the user by ID when deserializing.
//
// Both serializer and deserializer edited for Remember Me functionality
passport.serializeUser(function(user, done) {
var createAccessToken = function () {
var token = user.generateRandomToken();
User.findOne( { accessToken: token }, function (err, existingUser) {
if (err) { return done( err ); }
if (existingUser) {
createAccessToken(); // Run the function again - the token has to be unique!
} else {
user.set('accessToken', token);
user.save( function (err) {
if (err) return done(err);
return done(null, user.get('accessToken'));
})
}
});
};
if ( user._id ) {
createAccessToken();
}
});
passport.deserializeUser(function(token, done) {
User.findOne( {accessToken: token } , function (err, user) {
done(err, user);
});
});
// Use the LocalStrategy within Passport.
// Strategies in passport require a `verify` function, which accept
// credentials (in this case, a username and password), and invoke a callback
// with a user object. In the real world, this would query a database;
// however, in this example we are using a baked-in set of users.
passport.use(new LocalStrategy(function(username, password, done) {
User.findOne({ username: username }, function(err, user) {
if (err) { return done(err); }
if (!user) { return done(null, false, { message: 'Unknown user ' + username }); }
user.comparePassword(password, function(err, isMatch) {
if (err) return done(err);
if(isMatch) {
return done(null, user);
} else {
return done(null, false, { message: 'Invalid password' });
}
});
});
}));
var app = express();
// configure Express
app.configure(function() {
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.engine('ejs', require('ejs-locals'));
app.use(express.logger());
app.use(express.cookieParser());
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(express.session({
store: new RedisStore({ host: '127.0.0.1', port: 6379, prefix: 'chs-sess' }),
secret: '4Md97L1bL4r42SPn7076j1FwZvAiqube',
maxAge: new Date(Date.now() + 3600000)
}));
// Remember Me middleware
app.use( function (req, res, next) {
if ( req.method == 'POST' && req.url == '/login' ) {
if ( req.body.rememberme ) {
req.session.cookie.maxAge = 2592000000; // 30*24*60*60*1000 Rememeber 'me' for 30 days
} else {
req.session.cookie.expires = false;
}
}
next();
});
// Initialize Passport! Also use passport.session() middleware, to support
// persistent login sessions (recommended).
app.use(passport.initialize());
app.use(passport.session());
app.use(app.router);
app.use(express.static(__dirname + '/../../public'));
});
app.get('/', function(req, res){
res.render('index', { user: req.user });
});
app.get('/account', ensureAuthenticated, function(req, res){
res.render('account', { user: req.user });
});
app.get('/login', function(req, res){
res.render('login', { user: req.user, message: req.session.messages });
});
// POST /login
// Use passport.authenticate() as route middleware to authenticate the
// request. If authentication fails, the user will be redirected back to the
// login page. Otherwise, the primary route function function will be called,
// which, in this example, will redirect the user to the home page.
//
// curl -v -d "username=bob&password=secret" http://127.0.0.1:3000/login
//
/***** This version has a problem with flash messages
app.post('/login',
passport.authenticate('local', { failureRedirect: '/login', failureFlash: true }),
function(req, res) {
res.redirect('/');
});
*/
// POST /login
// This is an alternative implementation that uses a custom callback to
// acheive the same functionality.
app.post('/login', function(req, res, next) {
passport.authenticate('local', function(err, user, info) {
if (err) { return next(err) }
if (!user) {
req.session.messages = [info.message];
return res.redirect('/login')
}
req.logIn(user, function(err) {
if (err) { return next(err); }
return res.redirect('/');
});
})(req, res, next);
});
app.get('/logout', function(req, res){
req.logout();
res.redirect('/');
});
app.listen(3000, function() {
console.log('Express server listening on port 3000');
});
// Simple route middleware to ensure user is authenticated.
// Use this route middleware on any resource that needs to be protected. If
// the request is authenticated (typically via a persistent login session),
// the request will proceed. Otherwise, the user will be redirected to the
// login page.
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated()) { return next(); }
res.redirect('/login')
}
My app doesn't work with Remember Me, every time when I close the browser, I have to sign-in again. I don't know, what I have done wrong.
My second question is, how doe Remember Me works as usual? I have some idea but not exactly sure.