diff --git a/app/models/Invite.js b/app/models/Invite.js index 2a6dfee..dd5bcec 100755 --- a/app/models/Invite.js +++ b/app/models/Invite.js @@ -14,4 +14,8 @@ var InviteSchema = mongoose.Schema({ exp: Date }); +InviteSchema.methods.use = function(canonicalname, cb) { + return this.model('Invite').updateOne({code: this.code}, {recipient: canonicalname, used: Date.now()}, cb); +}; + module.exports = mongoose.model('Invite', InviteSchema); \ No newline at end of file diff --git a/app/models/User.js b/app/models/User.js index f979948..5e4c514 100755 --- a/app/models/User.js +++ b/app/models/User.js @@ -7,6 +7,11 @@ var UserSchema = mongoose.Schema({ unique: true, required: true }, + canonicalname: { + type: String, + unique: true, + required: true + }, scope: [String], uploadCount: { type: Number, @@ -19,6 +24,6 @@ var UserSchema = mongoose.Schema({ date: Date }); -UserSchema.plugin(passportLocalMongoose); +UserSchema.plugin(passportLocalMongoose, {usernameField: 'canonicalname'}); module.exports = mongoose.model('User', UserSchema); \ No newline at end of file diff --git a/app/routes/auth.js b/app/routes/auth.js index d67a82a..3de29fa 100755 --- a/app/routes/auth.js +++ b/app/routes/auth.js @@ -6,57 +6,124 @@ var Invite = require('../models/Invite.js'); var passport = require('passport'); -function checkInvite(code, callback) { +var async = require('async'); + +// Normalizes, decomposes, and lowercases a unicode string +function canonicalize(username) { + return username.normalize('NFKD').toLowerCase(); +} + +// Checks if an invite code is valid +// Returns the invite object if valid +function checkInvite(code, cb) { Invite.findOne({code: code}, function (err, invite) { - if (err) return callback(err); - if (!invite || invite.used || invite.exp < new Date()) - callback(null, false); + if (err) + cb(err); + else if (!invite) + cb('Invalid invite code.'); + else if (invite.used) + cb('Invite already used.'); + else if (invite.exp < Date.now()) + cb('Invite expired.'); else - callback(null, true, invite); + cb(null, invite); }); } -function useInvite(code, username) { - Invite.updateOne({code: code}, {recipient: username, used: new Date()}, function (err) { - if (err) throw err; +// Validates the username, then registers the user in the database using the given invite. +function registerUser(username, password, invite, sanitizeFn, cb) { + async.series([ + function (cb) { + // Canonicalize and sanitize the username, checking for HTML + var canonicalName = canonicalize(username); + var sanitizedName = sanitizeFn(canonicalName); + + if (sanitizedName !== canonicalName) + cb('Username failed sanitization check.'); + else if (canonicalName.length > 36) + cb('Username too long.'); + else + cb(null); + }, + function (cb) { + User.register(new User({ + username: username, + canonicalname: canonicalize(username), + scope: invite.scope, + date: Date.now() + }), password, cb); + }, + function (cb) { + invite.use(canonicalize(username), cb); + } + ], function (err) { + cb(err); }); } -router.post('/register', function (req, res, next) { - // Validate the invite code, then hand off to passport - checkInvite(req.body.invite, function (err, valid, invite) { - if (valid) { - User.register( - new User({username: req.body.username, scope: invite.scope, date: Date.now()}), - req.body.password, - function (err) { - if (err) return res.status(403).json({'message': err.message}); - passport.authenticate('local')(req, res, function () { - req.session.save(function(err) { - if (err) return next(err); - useInvite(req.body.invite, req.body.username); - req.session.username = req.body.username; - res.status(200).json({'message': 'Registered.'}); - }); - }); - } - ); +// Authenticates and creates the required session variables +function setupSession(username, req, res, cb) { + // Body needs to contain canonical name for proper authentication + req.body.canonicalname = canonicalize(req.body.username); + + passport.authenticate('local')(req, res, function () { + req.session.save(function (err) { + if (!err) { + req.session.username = username; + req.session.canonicalname = canonicalize(username); + } + cb(err); + }); + }); +} + +router.post('/register', function (req, res) { + async.waterfall([ + function (cb) { + checkInvite(req.body.invite, cb); + }, + function (invite, cb) { + registerUser(req.body.username, req.body.password, invite, req.sanitize, cb); + }, + function (cb) { + setupSession(req.body.username, req, res, cb); + } + ], function (err) { + if (err) { + res.status(401).json({'message': err}); } else { - res.status(401).json({'message': 'Invalid invite code.'}); + res.status(200).json({'message': 'Registration successful.'}); } }); }); router.post('/login', function (req, res, next) { - passport.authenticate('local', function(err, user, info) { - if (err) return next(err); - if (!user) return res.status(401).json({'message': info}); - req.logIn(user, function(err) { - if (err) return next(err); - req.session.username = user; - res.status(200).json({'message': 'Logged in.'}); - }); - })(req, res, next); + // Take 'username' from the form and canonicalize it for authentication. + req.body.canonicalname = canonicalize(req.body.username); + + async.waterfall([ + function (cb) { + passport.authenticate('local', function(err, user, info) { + cb(err, user, info); + })(req, res, next); + }, + function (user, info, cb) { + if (!user) + cb(info); + else + req.logIn(user, cb); + }, + function (cb) { + req.session.username = req.body.username; + req.session.canonicalname = canonicalize(req.body.username); + cb(); + } + ], function (err) { + if (err) + res.status(401).json({'message': err}); + else + res.status(200).json({'message': 'Login successful.'}); + }); }); router.get('/logout', function (req, res) { @@ -64,17 +131,18 @@ router.get('/logout', function (req, res) { res.status(200).json({'message': 'Logged out.'}); }); -router.get('/session', function(req, res) { - if (req.session.passport.user) { - User.findOne({username: req.session.passport.user}, function(err, user) { - res.status(200).json({ - user: user.username, - scope: user.scope - }); - }); - } else { - res.status(401).json({'message': 'Unauthorized.'}); - } +router.get('/session', function (req, res) { + if (req.session.passport.canonicalname) { + User.findOne({canonicalname: req.session.passport.canonicalname}, function (err, user) { + res.status(200).json({ + username: user.username, + canonicalname: user.canonicalname, + scope: user.scope + }); + }); + } else { + res.status(401).json({'message': 'Unauthorized.'}); + } }); module.exports = router; \ No newline at end of file diff --git a/server.js b/server.js index dff6036..baea354 100755 --- a/server.js +++ b/server.js @@ -50,7 +50,6 @@ app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.text()); app.use(sanitizer()); app.use(methodOverride('X-HTTP-Method-Override')); -app.use(passport.initialize()); //app.use(favicon(__dirname + '/public/img/favicon.ico'));