1 /*jshint node:true, indent:2, curly:false, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true,
2 regexp:true, undef:true, strict:true, trailing:true, white:true */
3 /*global X:true, _:true, SYS:true */
9 var recoverEmailText = "Follow this secure link to reset your password: " +
10 "https://%@/%@/recover/reset/%@/%@";
11 var systemErrorMessage = "A system error occurred. I'm very sorry about this, but I can't give " +
12 "you any more details because I'm very cautious about security and this is a sensitive topic.";
18 var passport = require('passport'),
20 setPassword = require('./change_password').setPassword,
21 utils = require('../oauth2/utils');
24 Receives user authentication credentials and have passport do the authentication.
27 //passport.authenticate('local', { successReturnToOrRedirect: '/login/scope', failureRedirect: '/', failureFlash: 'Invalid username or password.' }),
28 passport.authenticate('local', { failureRedirect: '/?login=fail' }),
29 function (req, res, next) {
31 if (req && req.session && !req.session.oauth2 && req.session.passport && req.session.passport.user && req.session.passport.user.organization) {
32 res.redirect("/" + req.session.passport.user.organization + '/app');
35 exports.scopeForm(req, res, next);
41 Renders the login form
43 exports.loginForm = function (req, res) {
46 if (req.query && req.query.login && req.query.login === 'fail') {
47 message = ["Invalid username or password."];
50 res.render('login', { message: message, databases: X.options.datasource.databases });
54 Renders the "forgot password?" form
56 exports.forgotPasswordForm = function (req, res) {
57 res.render('forgot_password', { message: [], databases: X.options.datasource.databases });
61 Create a row in the recover table, and send an email to the user with the token
62 whose hash is stored in the row, as well as the id to the row.
64 exports.recoverPassword = function (req, res) {
65 var userCollection = new SYS.UserCollection(),
66 email = req.body.email,
67 database = req.body.database,
68 errorMessage = "Cannot find email address",
69 successMessage = "An email has been sent with password recovery instructions";
71 if (!database || X.options.datasource.databases.indexOf(database) < 0) {
72 // don't show our hand
73 res.render('forgot_password', { message: [errorMessage], databases: X.options.datasource.databases });
78 // Find a user with the inputted email address. Make sure only one result is found.
80 userCollection.fetch({
88 username: X.options.databaseServer.user,
89 success: function (collection, results, options) {
90 var recoverModel = new SYS.Recover(),
93 if (results.length === 0) {
94 // XXX Ben recommends we don't show our hand here.
95 res.render('forgot_password', { message: [errorMessage], databases: X.options.datasource.databases });
97 } else if (results.length > 1) {
99 // errorMessage = "Wasn't expecting to see multiple users with this email address";
100 res.render('forgot_password', { message: [systemErrorMessage], databases: X.options.datasource.databases });
103 setRecovery = function () {
105 // We've initialized our recovery model. Now set and save it.
107 var uuid = utils.generateUUID(),
108 id = recoverModel.get("id"),
109 uuidHash = X.bcrypt.hashSync(uuid, 12),
111 tomorrow = new Date(now.getTime() + 1000 * 60 * 60 * 24),
113 recoverUsername: results[0].username,
114 hashedToken: uuidHash,
117 createdTimestamp: now,
118 expiresTimestamp: tomorrow
120 saveSuccess = function () {
122 // We've saved our recovery model. Now send out an email.
125 from: "no-reply@xtuple.com",
127 subject: "xTuple password reset instructions",
128 text: recoverEmailText.f(req.headers.host, database, id, uuid)
130 // XXX: don't log this
131 console.log(mailContent);
132 X.smtpTransport.sendMail(mailContent, function (err) {
134 // We've sent out the email. Now return to the user
137 res.render('forgot_password', { message: [systemErrorMessage],
138 databases: X.options.datasource.databases });
141 res.render('forgot_password', { message: [successMessage],
142 databases: X.options.datasource.databases });
145 saveError = function () {
146 res.render('forgot_password', { message: [errorMessage], databases: X.options.datasource.databases });
149 recoverModel.set(attributes);
150 recoverModel.save(null, {
152 username: X.options.databaseServer.user,
153 success: saveSuccess,
157 recoverModel.on('change:id', setRecovery);
158 recoverModel.initialize(null, {isNew: true, database: database});
161 res.render('forgot_password', { message: [errorMessage], databases: X.options.datasource.databases });
167 Validates the link that the user clicks on when they receive their email.
168 The token in the URL should hash to the hashed value in the table row
169 specified by the ID of the email. If everything checks out, forward them
170 to a screen to reset their password.
172 exports.verifyRecoverPassword = function (req, res) {
173 var error = function () {
174 res.render('forgot_password', { message: [systemErrorMessage], databases: X.options.datasource.databases });
176 recoveryModel = new SYS.Recover();
179 // We get the id and the unencrypted token from the URL. Make sure that the token checks out
180 // to that id in the database.
182 recoveryModel.fetch({
184 database: req.params.org,
185 username: X.options.databaseServer.user,
186 success: function (model, result, options) {
187 var now = new Date();
189 X.bcrypt.compare(req.params.token, model.get("hashedToken"), function (err, compare) {
192 model.get("accessed") ||
193 model.get("reset") ||
194 now.getTime() > model.get("expiresTimestamp").getTime()) {
196 // TODO: get the paths straight
197 res.render('forgot_password', { message: [systemErrorMessage], databases: X.options.datasource.databases });
202 // This is a valid recovery model. Update it as accessed, and
203 // set recovery variables in the user's session.
205 req.session.recover = {
207 token: req.params.token
211 accessedTimestamp: now,
212 expiresTimestamp: new Date(now.getTime() + 1000 * 60 * 15), // 15 minutes
213 ip: req.connection.remoteAddress
215 recoveryModel.save(null, {
216 database: req.params.org,
217 username: X.options.databaseServer.user,
219 success: function (model, result, options) {
220 res.render('reset_password', {message: []});
230 Handles the form submission to reset the password. Makes sure that the session is
231 the same one that we recently validate by re-validating. If everything checks out,
232 disable the validation row (by setting reset:true), update the user's password,
233 and redirect to the login page.
235 exports.resetRecoveredPassword = function (req, res) {
236 var error = function () {
237 res.render('forgot_password', { message: [systemErrorMessage], databases: X.options.datasource.databases });
239 recoveryModel = new SYS.Recover();
241 if (req.body.password !== req.body.password2) {
242 res.render('reset_password', {message: ["Passwords do not match"]});
246 // We get the id and the unencrypted token from the session.
247 // Make sure that the token checks out to that id in the database.
249 recoveryModel.fetch({
250 id: req.session.recover.id,
251 database: req.params.org,
252 username: X.options.databaseServer.user,
253 success: function (model, result, options) {
254 var now = new Date();
256 X.bcrypt.compare(req.session.recover.token, model.get("hashedToken"), function (err, compare) {
259 !model.get("accessed") ||
260 model.get("reset") ||
261 model.get("ip") !== req.connection.remoteAddress ||
262 now.getTime() > model.get("expiresTimestamp").getTime()) {
264 // TODO: get the paths straight
265 res.render('forgot_password', { message: [systemErrorMessage], databases: X.options.datasource.databases });
270 // This is a valid recovery model. Update it as reset.
276 recoveryModel.save(null, {
277 database: req.params.org,
278 username: X.options.databaseServer.user,
280 success: function (model, result, options) {
281 var userModel = new SYS.User();
283 id: model.get("recoverUsername"),
284 database: req.params.org,
285 username: X.options.databaseServer.user,
287 success: function (model, result, options) {
289 // NOW we update the user's password
291 setPassword(recoveryModel.get("recoverUsername"),
294 model.get("useEnhancedAuth"),
301 // The password has been updated. Redirect the user to the login screen.
303 // TODO: get the path right
304 res.render('login', {
305 message: ["Your password has been updated. Please log in."],
306 databases: X.options.datasource.databases
320 Logs out user by removing the session and sending the user to the login screen.
322 exports.logout = function (req, res) {
323 if (req.session.passport) {
324 // Make extra sure passport is empty.
325 req.session.passport = null;
329 // Kill the whole session, db, cache and all.
330 req.session.destroy(function () {});
333 if (req.path.split("/")[1]) {
334 res.clearCookie(req.path.split("/")[1] + ".sid");
342 Receives a request telling us which organization a user has selected
343 to log into. Note that we don't trust the client; we check
344 to make sure that the user actually belongs to that organization.
346 exports.scope = function (req, res, next) {
347 var userId = req.session.passport.user.id,
348 selectedOrg = req.body.org,
349 user = new SYS.User(),
352 options.success = function (response) {
357 if (response.length === 0) {
358 if (req.session && req.session.oauth2 && req.session.oauth2.redirectURI) {
359 X.log("OAuth 2.0 User %@ has no business trying to log in to organization %@.".f(userId, selectedOrg));
360 res.redirect(req.session.oauth2.redirectURI + '?error=access_denied');
364 X.log("User %@ has no business trying to log in to organization %@.".f(userId, selectedOrg));
365 res.redirect('/' + selectedOrg + '/logout');
367 } else if (response.length > 1) {
368 X.log("More than one User: %@ exists.".f(userId));
369 res.redirect('/' + selectedOrg + '/logout');
373 // We can now trust this user's request to log in to this organization.
375 // Update the session store row to add the org choice and username.
376 // Note: Updating this object magically persists the data into the SessionStore table.
378 //privs = _.map(response.get("privileges"), function (privAss) {
379 // return privAss.privilege.name;
382 //_.each(response.get('organizations'), function (orgValue, orgKey, orgList) {
383 // if (orgValue.name === selectedOrg) {
384 // userOrg = orgValue.name;
385 // userName = orgValue.username;
389 //if (!userOrg || !userName) {
390 if (!response.get("username")) {
391 // This shouldn't happen.
392 X.log("User %@ has no business trying to log in to organization %@.".f(userId, selectedOrg));
393 res.redirect('/' + selectedOrg + '/logout');
397 //req.session.passport.user.globalPrivileges = privs;
398 req.session.passport.user.organization = response.get("organization");
399 req.session.passport.user.username = response.get("username");
401 // TODO - req.oauth probably isn't enough here, but it's working 2013-03-15...
402 // If this is an OAuth 2.0 login with only 1 org.
407 // If this is an OAuth 2.0 login with more than 1 org.
408 if (req.session.returnTo) {
409 res.redirect(req.session.returnTo);
411 // Redirect to start loading the client app.
412 res.redirect('/' + selectedOrg + '/app');
416 options.error = function (model, error) {
417 X.log("userorg fetch error", error);
418 res.redirect('/' + selectedOrg + '/logout');
423 // The user id we're searching for.
426 // The user under whose authority the query is run.
427 options.username = X.options.databaseServer.user;
428 options.database = selectedOrg;
430 // Verify that the org is valid for the user.
435 Loads the form to let the user choose their organization. If there's only one
436 organization for the user we choose for them.
438 exports.scopeForm = function (req, res, next) {
439 var organizations = [],
444 organizations = _.map(req.user.get("organizations"), function (org) {
448 // Prevent unauthorized access.
453 // If this is an OAuth 2.0 login req, try and get the org from the requested scope.
454 if (req.session && req.session.oauth2) {
456 if (req.session.oauth2.req && req.session.oauth2.req.scope && req.session.oauth2.req.scope.length > 0) {
457 // Loop through the scope URIs and convert them to org names.
458 _.each(req.session.oauth2.req.scope, function (value, key, list) {
461 // Get the org from the scope URI e.g. 'dev' from: 'https://mobile.xtuple.com/auth/dev'
462 scope = url.parse(value, true);
463 org = scope.path.split("/")[1] || null;
465 // TODO - Still need more work to support userinfo calls.
466 // See node-datasource/oauth2/oauth2.js authorization.
468 // The scope 'https://mobile.xtuple.com/auth/userinfo.xxx' can be used to make userinfo
469 // REST calls and is not a valid org scope, we'll skip it here.
470 if (org && org.indexOf('userinfo') === -1) {
475 // If we only have one scope/org sent, choose it for this request.
476 if (scopes.length === 1) {
477 req.body.org = scopes[0];
478 exports.scope(req, res, next);
482 // TODO - Multiple scopes sent.
483 // Do we want to let them select an org or respond with error and scopeList?
484 // It depends on the scenario. Some support user interaction and can select an org, others
485 // do not and should get an error.
489 // TODO - No scope is sent.
490 // Do we want to let them select an org or respond with error and scopeList?
491 // It depends on the scenario. Some support user interaction and can select an org, others
492 // do not and should get an error.
496 // Below will handle OAuth "TODO - Multiple scopes sent", "TODO - No scope is sent." above for now.
498 // Choose an org automatically if there's only one for this user.
499 if (organizations.length === 1) {
500 req.body.org = organizations[0];
501 exports.scope(req, res, next);
505 // Some users may not have any orgs. They should not get this far.
506 if (organizations.length === 0) {
507 X.err("User: %@ shall not pass, they have no orgs to select.".f(req.session.passport.user.id));
508 req.flash('orgerror', 'You have not been assigned to any organizations.');
511 // We've got nothing, let the user choose their scope/org.
512 res.render('scope', { organizations: organizations.sort(), message: req.flash('orgerror') });