authentication/Authentication.js

  1. /* globals window, document */
  2. var FieldDBObject = require("./../FieldDBObject").FieldDBObject;
  3. var Database = require("./../corpus/Database").Database;
  4. var CORS = Database.CORS;
  5. // var UserMask = require("./../user/UserMask").UserMask;
  6. var User = require("./../user/User").User;
  7. var Confidential = require("./../confidentiality_encryption/Confidential").Confidential;
  8. var Q = require("q");
  9. var Connection = require("./../corpus/Connection").Connection;
  10. // var md5 = require("md5");
  11. var bcrypt = require("bcrypt-nodejs");
  12. // console.log(bcrypt.hashSync("phoneme", "$2a$10$UsUudKMbgfBQzn5SDYWyFe"));
  13. /**
  14. * @class The Authentication Model handles login and logout and
  15. * authentication locally or remotely. *
  16. *
  17. * @property {User} user The user is a User object (User, Bot or Consultant)
  18. * which is logged in and viewing the app with that user's
  19. * perspective. To check whether some data is
  20. * public/viewable/editable the app.user should be used to verify
  21. * the permissions. If no user is logged in a special user
  22. * "public" is logged in and used to calculate permissions.
  23. *
  24. * @extends FieldDBObject
  25. * @tutorial tests/authentication/AuthenticationTest.js
  26. */
  27. var Authentication = function Authentication(options) {
  28. if (!this._fieldDBtype) {
  29. this._fieldDBtype = "Authentication";
  30. }
  31. this.debug("Constructing a Authentication " + options);
  32. var self = this;
  33. this.loading = true;
  34. var deferred = new Q.defer();
  35. this.resumingSessionPromise = deferred.promise;
  36. Database.prototype.resumeAuthenticationSession().then(function(user) {
  37. CORS.application = FieldDBObject.application;
  38. self.loading = false;
  39. self.debug(user);
  40. self.user = user;
  41. self.user.fetch();
  42. if (self.user._rev) {
  43. self.user.authenticated = true;
  44. self.dispatchEvent("authenticateSuccess");
  45. deferred.resolve(self.user);
  46. } else {
  47. self.user.authenticated = false;
  48. self.dispatchEvent("authenticateMustConfirmIdentity");
  49. deferred.reject({
  50. status: 401,
  51. userFriendlyErrors: ["Please login."]
  52. });
  53. }
  54. // if (sessionInfo.ok && sessionInfo.userCtx.name) {
  55. // selfauthentication.user.username = sessionInfo.userCtx.name;
  56. // selfauthentication.user.roles = sessionInfo.userCtx.roles;
  57. // processUserDetails(selfauthentication.user);
  58. // } else {
  59. // if (window.location.pathname.indexOf("welcome") < 0 && window.location.pathname.indexOf("bienvenu") < 0) {
  60. // $scope.$apply(function() {
  61. // // $location.path(selfbasePathname + "/#/welcome/", false);
  62. // window.location.replace(selfbasePathname + "/#/welcome");
  63. // });
  64. // }
  65. // }
  66. return self.user;
  67. }, function(error) {
  68. // Wait and see if a login call is coming...
  69. Q.nextTick(function() {
  70. if (self.loggingIn) {
  71. deferred.resolve(self.user);
  72. return;
  73. }
  74. self.loading = false;
  75. self.warn("Unable to resume login " + error.userFriendlyErrors.join(" "));
  76. if (error.status === 401) {
  77. self.dispatchEvent("authenticateMustConfirmIdentity");
  78. } else {
  79. // error.userFriendlyErrors = ["Unable to resume session, are you sure you're not offline?"];
  80. self.error = error.userFriendlyErrors.join(" ");
  81. // self.dispatchEvent("authenticateMustConfirmIdentity");
  82. }
  83. self.render();
  84. deferred.reject(error);
  85. });
  86. return error;
  87. }).fail(function(error) {
  88. console.error(error.stack, self);
  89. deferred.reject(error);
  90. return error;
  91. });
  92. FieldDBObject.apply(this, arguments);
  93. };
  94. Authentication.prototype = Object.create(FieldDBObject.prototype, /** @lends Authentication.prototype */ {
  95. constructor: {
  96. value: Authentication
  97. },
  98. // Internal models: used by the parse function
  99. INTERNAL_MODELS: {
  100. value: {
  101. user: User,
  102. confidential: Confidential
  103. }
  104. },
  105. dispatchEvent: {
  106. value: function(eventChannelName, reason) {
  107. try {
  108. if (this.eventDispatcher && typeof this.eventDispatcher.trigger === "function") {
  109. this.eventDispatcher.trigger(eventChannelName, reason);
  110. } else {
  111. this.eventDispatcher = this.eventDispatcher || document;
  112. var event = this.eventDispatcher.createEvent("Event");
  113. event.initEvent(eventChannelName, true, true);
  114. this.eventDispatcher.dispatchEvent(event);
  115. }
  116. } catch (e) {
  117. this.warn("Cant dispatch event " + eventChannelName + " the document element isn't available.");
  118. this.debug(" error ", e);
  119. }
  120. }
  121. },
  122. /**
  123. * Contacts local or remote server to verify the username and password
  124. * provided in the user object. Upon success, calls the callback with the
  125. * user.
  126. *
  127. * @param user A user object to verify against the authentication database
  128. * @param callback A callback to call upon sucess.
  129. */
  130. login: {
  131. value: function(loginDetails) {
  132. var deferred = Q.defer(),
  133. self = this;
  134. if (this.whenLoggedIn){
  135. return this.whenLoggedIn;
  136. }
  137. this.whenLoggedIn = deferred.promise;
  138. var dataToPost = {};
  139. dataToPost.username = loginDetails.username;
  140. dataToPost.password = loginDetails.password;
  141. dataToPost.authUrl = loginDetails.authUrl;
  142. dataToPost.connection = loginDetails.connection;
  143. if (!loginDetails.syncUserDetails) {
  144. //if the same user is re-authenticating, include their details to sync to the server.
  145. var tempUser = new User(loginDetails);
  146. tempUser.fetch();
  147. if (tempUser._rev && tempUser.username !== "public" && !tempUser.fetching && !tempUser.loading && tempUser.lastSyncWithServer) {
  148. dataToPost.syncDetails = "true";
  149. dataToPost.syncUserDetails = tempUser.toJSON();
  150. tempUser.warn("Backing up tempUser details", dataToPost.syncUserDetails);
  151. delete dataToPost.syncUserDetails._rev;
  152. //TODO what if they log out, when they have change to their private data that hasnt been pushed to the server,
  153. //the server will overwrite their details.
  154. //should we automatically check here, or should we make htem a button
  155. //when they are authetnticated to test if they ahve lost their prefs etc?
  156. }
  157. }
  158. this.error = "";
  159. this.status = "";
  160. this.loading = this.loggingIn = true;
  161. var handleFailedLogin = function(error) {
  162. self.loading = false;
  163. if (self.user) {
  164. self.user.authenticated = false;
  165. }
  166. if (!error || !error.userFriendlyErrors) {
  167. error.userFriendlyErrors = ["Unknown error. Please report this 2456."];
  168. self.dispatchEvent("authenticateMustConfirmIdentity");
  169. } else {
  170. self.dispatchEvent("authenticateFail", error);
  171. }
  172. error.details = loginDetails;
  173. self.warn("Logging in failed: " + error.status, error.userFriendlyErrors);
  174. self.error = error.userFriendlyErrors.join(" ");
  175. deferred.reject(error);
  176. delete self.whenLoggedIn;
  177. delete self.loggingIn;
  178. };
  179. self.resumingSessionPromise = deferred.promise;
  180. Database.prototype.login(dataToPost)
  181. .then(function(userDetails) {
  182. if (!userDetails) {
  183. self.loading = false;
  184. self.dispatchEvent("authenticateMustConfirmIdentity");
  185. deferred.reject({
  186. details: loginDetails,
  187. status: 500,
  188. userFriendlyErrors: ["Unknown error. Please report this 2391."]
  189. });
  190. return;
  191. }
  192. try {
  193. self.user = userDetails;
  194. self.user.lastSyncWithServer = Date.now();
  195. } catch (e) {
  196. console.warn("There was a problem assigning the user. ", e);
  197. }
  198. self.authenticateWithAllCorpusServers(loginDetails).then(function() {
  199. self.loading = false;
  200. self.user.authenticated = true;
  201. self.dispatchEvent("authenticateSuccess");
  202. deferred.resolve(self.user);
  203. }, function() {
  204. self.loading = false;
  205. deferred.resolve(self.user);
  206. }).fail(function(error) {
  207. self.loading = false;
  208. console.error(error.stack, self);
  209. deferred.resolve(self.user);
  210. });
  211. delete self.loggingIn;
  212. }, //end successful login
  213. handleFailedLogin)
  214. .fail(function(error) {
  215. delete self.loggingIn;
  216. console.error(error.stack, self);
  217. handleFailedLogin(error);
  218. });
  219. return this.whenLoggedIn;
  220. }
  221. },
  222. confirmIdentity: {
  223. value: function(loginDetails) {
  224. var deferred = Q.defer(),
  225. self = this;
  226. if (!loginDetails || !loginDetails.password) {
  227. Q.nextTick(function() {
  228. deferred.reject({
  229. userFriendlyErrors: ["You must enter your password to confirm your identity."]
  230. });
  231. });
  232. return deferred.promise;
  233. }
  234. if (!this.user.username ||
  235. !this.user.authenticated ||
  236. !this.user.lastSyncWithServer ||
  237. !this.user.hash ||
  238. !this.user.salt) {
  239. Q.nextTick(function() {
  240. deferred.reject({
  241. userFriendlyErrors: "You must login first."
  242. });
  243. });
  244. return deferred.promise;
  245. }
  246. try {
  247. loginDetails.password = (loginDetails.password + "").trim();
  248. bcrypt.compare(loginDetails.password, self.user.hash, function(err, confirmed) {
  249. if (confirmed) {
  250. loginDetails.info = ["Verified offline."];
  251. deferred.resolve(loginDetails);
  252. } else {
  253. loginDetails.error = err;
  254. if (err) {
  255. loginDetails.userFriendlyErrors = ["This app has errored while trying to confirm your identity. Please report this 2892346."];
  256. } else {
  257. loginDetails.userFriendlyErrors = ["Sorry, this doesn't appear to be you."];
  258. }
  259. deferred.reject(loginDetails);
  260. }
  261. });
  262. } catch (e) {
  263. loginDetails.userFriendlyErrors = ["This app has errored while trying to confirm your identity. Please report this 289234."];
  264. deferred.reject(loginDetails);
  265. }
  266. return deferred.promise;
  267. }
  268. },
  269. authenticateWithAllCorpusServers: {
  270. value: function(loginDetails) {
  271. var deferred = Q.defer(),
  272. self = this,
  273. corpusServersWhichHouseUsersCorpora = [],
  274. promises = [];
  275. if ((!this.user.corpora || this.user.corpora.length === 0) && !loginDetails.connection) {
  276. Q.nextTick(function() {
  277. self.bug("You don't have access to any corpora. This is strange.");
  278. deferred.resolve(self.user);
  279. });
  280. return deferred.promise;
  281. }
  282. if (loginDetails.connection) {
  283. corpusServersWhichHouseUsersCorpora.push(loginDetails.connection.corpusUrl);
  284. }
  285. self.debug("loginDetails.connection", loginDetails.connection);
  286. this.user.corpora.map(function(connection) {
  287. var addThisServerIfNotAlreadyThere = function(url) {
  288. var couchdbSessionUrl = url.replace(connection.dbname, "_session");
  289. if (corpusServersWhichHouseUsersCorpora.indexOf(couchdbSessionUrl) === -1) {
  290. corpusServersWhichHouseUsersCorpora.push(couchdbSessionUrl);
  291. }
  292. //old logic from database.
  293. // if (!self.dbname && corpusServersWhichHouseUsersCorpora.indexOf(couchdbSessionUrl) === -1) {
  294. // corpusServersWhichHouseUsersCorpora.push(couchdbSessionUrl);
  295. // } else if (self.dbname && connection.dbname === self.dbname && corpusServersWhichHouseUsersCorpora.indexOf(couchdbSessionUrl) === -1) {
  296. // corpusServersWhichHouseUsersCorpora.push(couchdbSessionUrl);
  297. // }
  298. };
  299. if (connection.corpusUrls) {
  300. connection.corpusUrls.map(addThisServerIfNotAlreadyThere);
  301. } else {
  302. addThisServerIfNotAlreadyThere(connection.corpusUrl);
  303. }
  304. });
  305. if (corpusServersWhichHouseUsersCorpora.length < 1) {
  306. this.bug("You don't have access to any corpora. This is strange.");
  307. }
  308. this.debug("Requesting session token for all corpora user has access to.");
  309. this.user.roles = [];
  310. for (var corpusUrlIndex = 0; corpusUrlIndex < corpusServersWhichHouseUsersCorpora.length; corpusUrlIndex++) {
  311. promises.push(Database.prototype.login({
  312. authUrl: corpusServersWhichHouseUsersCorpora[corpusUrlIndex],
  313. name: this.user.username,
  314. password: loginDetails.password
  315. }));
  316. }
  317. Q.allSettled(promises).then(function(results) {
  318. var anySucceeded = false;
  319. var errorReason = {};
  320. var allRoles = [];
  321. results.map(function(result) {
  322. self.debug("some roles", result);
  323. if (result.state === "fulfilled" && result.value && result.value.roles) {
  324. allRoles = allRoles.concat(result.value.roles);
  325. anySucceeded = true;
  326. } else {
  327. self.debug("Failed to login to one of the users's corpus servers ", result);
  328. if (result.reason && result.reason.status !== undefined) {
  329. errorReason.status = result.reason.status;
  330. }
  331. if (result.reason && result.reason.userFriendlyErrors) {
  332. errorReason.userFriendlyErrors = result.reason.userFriendlyErrors;
  333. }
  334. if (result.reason && result.reason.details) {
  335. errorReason.details = result.reason.details;
  336. }
  337. }
  338. self.debug("some roles", allRoles);
  339. });
  340. var roles = {};
  341. self.user.roles = [];
  342. allRoles.map(function(role) {
  343. if (role && !roles[role]) {
  344. roles[role] = 1;
  345. self.user.roles.push(role);
  346. }
  347. });
  348. if (anySucceeded) {
  349. deferred.resolve(self.user);
  350. } else {
  351. errorReason.all = results;
  352. deferred.reject(errorReason);
  353. }
  354. });
  355. // .then(function(sessionInfo) {
  356. // // self.debug(sessionInfo);
  357. // result.user.roles = sessionInfo.roles;
  358. // deferred.resolve(result.user);
  359. // }, function() {
  360. // self.debug("Failed to login ");
  361. // deferred.reject("Something is wrong.");
  362. // });
  363. return deferred.promise;
  364. }
  365. },
  366. register: {
  367. value: function(options) {
  368. var self = this,
  369. deferred = Q.defer();
  370. this.loading = true;
  371. Database.prototype.register(options).then(function(userDetails) {
  372. self.debug("registration succeeeded, waiting to login ", userDetails);
  373. // self.user = userDetails;
  374. var waitTime = 1000;
  375. var loopExponentialDecayLogin = function(options) {
  376. self.login(options).
  377. then(function(results) {
  378. self.debug(" login after registration is complete " + waitTime, results);
  379. deferred.resolve(results);
  380. }, function(error) {
  381. waitTime = waitTime * 2;
  382. error.details = options;
  383. if (waitTime > 60 * 1000) {
  384. deferred.reject(error);
  385. } else {
  386. self.debug(" waiting to login " + waitTime, error);
  387. self.loading = true;
  388. setTimeout(function() {
  389. loopExponentialDecayLogin(options);
  390. }, waitTime);
  391. }
  392. }).fail(
  393. function(error) {
  394. console.error(error.stack, self);
  395. deferred.reject(error);
  396. });
  397. };
  398. loopExponentialDecayLogin(options);
  399. }, function(error) {
  400. self.loading = false;
  401. self.debug("registration failed ", error);
  402. deferred.reject(error);
  403. });
  404. return deferred.promise;
  405. }
  406. },
  407. logout: {
  408. value: function(options) {
  409. var self = this;
  410. this.loading = true;
  411. this.save();
  412. options = options || {};
  413. return Database.prototype.logout(options.url).then(function() {
  414. self.dispatchEvent("logout");
  415. self.loading = false;
  416. if (options && !options.letClientHandleCleanUp) {
  417. self.warn("Reloading the page");
  418. try {
  419. window.location.reload();
  420. } catch (e) {
  421. self.debug("Window is undefined", e);
  422. }
  423. }
  424. });
  425. }
  426. },
  427. /**
  428. * This function parses the server response and injects it into the authentication's user public and user private
  429. *
  430. */
  431. user: {
  432. get: function() {
  433. return this._user;
  434. },
  435. set: function(value) {
  436. if (!value) {
  437. return;
  438. }
  439. var overwriteOrNot;
  440. this.debug("setting user");
  441. // if (!(value instanceof User)) {
  442. // value = new User(value);
  443. // }
  444. if (this._user && this._user.username === value.username) {
  445. if (!this._user.rev) {
  446. this.debug("Fetching the user's full details");
  447. this._user.fetch();
  448. }
  449. this.debug("Merging the user", this._user, value);
  450. if (!(value instanceof User)) {
  451. value = new User(value);
  452. }
  453. if (!this._user._rev) {
  454. overwriteOrNot = "overwrite";
  455. }
  456. this._user.merge("self", value, overwriteOrNot);
  457. } else {
  458. if (!(value instanceof User)) {
  459. value = new User(value);
  460. }
  461. this.debug("Setting the user");
  462. this._user = value;
  463. }
  464. var self = this;
  465. this._user.save().then(function() {
  466. self.debug("Saved user ");
  467. });
  468. this._user.render();
  469. }
  470. },
  471. save: {
  472. value: function() {
  473. return this.user.save();
  474. }
  475. },
  476. /**
  477. * This function uses the quick authentication view to get the user's
  478. * password and authenticate them. The authenticate process brings down the
  479. * user from the server, and also gets their sesson token from couchdb
  480. * before calling the callback.
  481. *
  482. * If there is no quick authentication view it takes them either to the user
  483. * page (in the ChromeApp) or the public user page (in a couchapp) where
  484. * they dont have to have a corpus token to see the data, and log in
  485. *
  486. * @param callback
  487. * a success callback which is called once the user has been backed
  488. * up to the server, and their couchdb session token is ready to be
  489. * used to contact the database.
  490. * @param corpusPouchName
  491. * an optional corpus pouch name to redirect the user to if they
  492. * end up geting kicked out of the corpus page
  493. */
  494. syncUserWithServer: {
  495. value: function() {
  496. var self = this;
  497. this.todo("will this return a promise.");
  498. return this.renderQuickAuthentication()
  499. .then(function(userinfo) {
  500. self.login(userinfo);
  501. })
  502. .fail(function(error) {
  503. console.error(error.stack, self);
  504. });
  505. }
  506. },
  507. newCorpus: {
  508. value: function(details) {
  509. var deferred = Q.defer(),
  510. self = this;
  511. Q.nextTick(function() {
  512. if (!details) {
  513. deferred.reject({
  514. details: details,
  515. userFriendlyErrors: ["This application has errored, please contact us."],
  516. status: 412
  517. });
  518. return;
  519. }
  520. details.authUrl = Database.prototype.deduceAuthUrl(details.authUrl);
  521. if (!details.username) {
  522. deferred.reject({
  523. details: details,
  524. userFriendlyErrors: ["Please supply a username."],
  525. status: 412
  526. });
  527. return;
  528. }
  529. if (!details.password) {
  530. deferred.reject({
  531. details: details,
  532. userFriendlyErrors: ["You must enter your password to prove that that this is you."],
  533. status: 412
  534. });
  535. return;
  536. }
  537. details.title = details.title || details.newCorpusTitle;
  538. details.newCorpusTitle = details.title;
  539. if (!details.title) {
  540. deferred.reject({
  541. details: details,
  542. userFriendlyErrors: ["Please supply a title for your new corpus."],
  543. status: 412
  544. });
  545. return;
  546. }
  547. var validateUsername = Connection.validateUsername(details.username);
  548. if (validateUsername.changes.length > 0) {
  549. details.username = validateUsername.identifier;
  550. self.warn(" Invalid username ", validateUsername.changes.join("\n "));
  551. deferred.reject({
  552. error: validateUsername,
  553. userFriendlyErrors: validateUsername.changes,
  554. status: 412
  555. });
  556. return;
  557. }
  558. CORS.makeCORSRequest({
  559. type: "POST",
  560. dataType: "json",
  561. url: details.authUrl + "/newcorpus",
  562. data: details
  563. }).then(function(authserverResult) {
  564. self.debug(authserverResult);
  565. if (authserverResult.corpus) {
  566. self.user.corpora.shift(authserverResult.corpus);
  567. self.save();
  568. } else {
  569. if (authserverResult.status > 0 && authserverResult.status < 400 && self.user.corpora && typeof self.user.corpora.find === "function") {
  570. authserverResult.corpus = self.user.corpora.find("dbname", Connection.validateIdentifier(details.newCorpusTitle).identifier, "fuzzy");
  571. if (authserverResult.corpus && authserverResult.corpus.length > 0) {
  572. authserverResult.corpus = authserverResult.corpus[0];
  573. }
  574. }
  575. }
  576. deferred.resolve(authserverResult.corpus);
  577. },
  578. function(reason) {
  579. reason = reason || {};
  580. reason.details = details;
  581. reason.userFriendlyErrors = reason.userFriendlyErrors || ["Unknown error, please report this."];
  582. self.debug(reason);
  583. deferred.reject(reason);
  584. }).fail(
  585. function(error) {
  586. console.error(error.stack, self);
  587. deferred.reject(error);
  588. });
  589. });
  590. return deferred.promise;
  591. }
  592. },
  593. toJSON: {
  594. value: function(includeEvenEmptyAttributes, removeEmptyAttributes) {
  595. this.debug("Customizing toJSON ", includeEvenEmptyAttributes, removeEmptyAttributes);
  596. var attributesNotToJsonify = ["resumingSessionPromise", "eventDispatcher"];
  597. var json = FieldDBObject.prototype.toJSON.apply(this, [includeEvenEmptyAttributes, removeEmptyAttributes, attributesNotToJsonify]);
  598. this.debug(json);
  599. return json;
  600. }
  601. }
  602. });
  603. exports.Authentication = Authentication;