PageRenderTime 63ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/packages/accounts-base/accounts_server.js

https://github.com/tiev/meteor
JavaScript | 1207 lines | 664 code | 149 blank | 394 comment | 93 complexity | b1c7fee6cba9b76d2ad0e7a75e91fd31 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. var crypto = Npm.require('crypto');
  2. ///
  3. /// CURRENT USER
  4. ///
  5. Meteor.userId = function () {
  6. // This function only works if called inside a method. In theory, it
  7. // could also be called from publish statements, since they also
  8. // have a userId associated with them. However, given that publish
  9. // functions aren't reactive, using any of the infomation from
  10. // Meteor.user() in a publish function will always use the value
  11. // from when the function first runs. This is likely not what the
  12. // user expects. The way to make this work in a publish is to do
  13. // Meteor.find(this.userId()).observe and recompute when the user
  14. // record changes.
  15. var currentInvocation = DDP._CurrentInvocation.get();
  16. if (!currentInvocation)
  17. throw new Error("Meteor.userId can only be invoked in method calls. Use this.userId in publish functions.");
  18. return currentInvocation.userId;
  19. };
  20. Meteor.user = function () {
  21. var userId = Meteor.userId();
  22. if (!userId)
  23. return null;
  24. return Meteor.users.findOne(userId);
  25. };
  26. ///
  27. /// LOGIN HOOKS
  28. ///
  29. // Exceptions inside the hook callback are passed up to us.
  30. var validateLoginHook = new Hook();
  31. // Callback exceptions are printed with Meteor._debug and ignored.
  32. var onLoginHook = new Hook({
  33. debugPrintExceptions: "onLogin callback"
  34. });
  35. var onLoginFailureHook = new Hook({
  36. debugPrintExceptions: "onLoginFailure callback"
  37. });
  38. Accounts.validateLoginAttempt = function (func) {
  39. return validateLoginHook.register(func);
  40. };
  41. Accounts.onLogin = function (func) {
  42. return onLoginHook.register(func);
  43. };
  44. Accounts.onLoginFailure = function (func) {
  45. return onLoginFailureHook.register(func);
  46. };
  47. // Give each login hook callback a fresh cloned copy of the attempt
  48. // object, but don't clone the connection.
  49. //
  50. var cloneAttemptWithConnection = function (connection, attempt) {
  51. var clonedAttempt = EJSON.clone(attempt);
  52. clonedAttempt.connection = connection;
  53. return clonedAttempt;
  54. };
  55. var validateLogin = function (connection, attempt) {
  56. validateLoginHook.each(function (callback) {
  57. var ret;
  58. try {
  59. ret = callback(cloneAttemptWithConnection(connection, attempt));
  60. }
  61. catch (e) {
  62. attempt.allowed = false;
  63. // XXX this means the last thrown error overrides previous error
  64. // messages. Maybe this is surprising to users and we should make
  65. // overriding errors more explicit. (see
  66. // https://github.com/meteor/meteor/issues/1960)
  67. attempt.error = e;
  68. return true;
  69. }
  70. if (! ret) {
  71. attempt.allowed = false;
  72. // don't override a specific error provided by a previous
  73. // validator or the initial attempt (eg "incorrect password").
  74. if (!attempt.error)
  75. attempt.error = new Meteor.Error(403, "Login forbidden");
  76. }
  77. return true;
  78. });
  79. };
  80. var successfulLogin = function (connection, attempt) {
  81. onLoginHook.each(function (callback) {
  82. callback(cloneAttemptWithConnection(connection, attempt));
  83. return true;
  84. });
  85. };
  86. var failedLogin = function (connection, attempt) {
  87. onLoginFailureHook.each(function (callback) {
  88. callback(cloneAttemptWithConnection(connection, attempt));
  89. return true;
  90. });
  91. };
  92. ///
  93. /// LOGIN METHODS
  94. ///
  95. // Login methods return to the client an object containing these
  96. // fields when the user was logged in successfully:
  97. //
  98. // id: userId
  99. // token: *
  100. // tokenExpires: *
  101. //
  102. // tokenExpires is optional and intends to provide a hint to the
  103. // client as to when the token will expire. If not provided, the
  104. // client will call Accounts._tokenExpiration, passing it the date
  105. // that it received the token.
  106. //
  107. // The login method will throw an error back to the client if the user
  108. // failed to log in.
  109. //
  110. //
  111. // Login handlers and service specific login methods such as
  112. // `createUser` internally return a `result` object containing these
  113. // fields:
  114. //
  115. // type:
  116. // optional string; the service name, overrides the handler
  117. // default if present.
  118. //
  119. // error:
  120. // exception; if the user is not allowed to login, the reason why.
  121. //
  122. // userId:
  123. // string; the user id of the user attempting to login (if
  124. // known), required for an allowed login.
  125. //
  126. // options:
  127. // optional object merged into the result returned by the login
  128. // method; used by HAMK from SRP.
  129. //
  130. // stampedLoginToken:
  131. // optional object with `token` and `when` indicating the login
  132. // token is already present in the database, returned by the
  133. // "resume" login handler.
  134. //
  135. // For convenience, login methods can also throw an exception, which
  136. // is converted into an {error} result. However, if the id of the
  137. // user attempting the login is known, a {userId, error} result should
  138. // be returned instead since the user id is not captured when an
  139. // exception is thrown.
  140. //
  141. // This internal `result` object is automatically converted into the
  142. // public {id, token, tokenExpires} object returned to the client.
  143. // Try a login method, converting thrown exceptions into an {error}
  144. // result. The `type` argument is a default, inserted into the result
  145. // object if not explicitly returned.
  146. //
  147. var tryLoginMethod = function (type, fn) {
  148. var result;
  149. try {
  150. result = fn();
  151. }
  152. catch (e) {
  153. result = {error: e};
  154. }
  155. if (result && !result.type && type)
  156. result.type = type;
  157. return result;
  158. };
  159. // Log in a user on a connection.
  160. //
  161. // We use the method invocation to set the user id on the connection,
  162. // not the connection object directly. setUserId is tied to methods to
  163. // enforce clear ordering of method application (using wait methods on
  164. // the client, and a no setUserId after unblock restriction on the
  165. // server)
  166. //
  167. // The `stampedLoginToken` parameter is optional. When present, it
  168. // indicates that the login token has already been inserted into the
  169. // database and doesn't need to be inserted again. (It's used by the
  170. // "resume" login handler).
  171. var loginUser = function (methodInvocation, userId, stampedLoginToken) {
  172. if (! stampedLoginToken) {
  173. stampedLoginToken = Accounts._generateStampedLoginToken();
  174. Accounts._insertLoginToken(userId, stampedLoginToken);
  175. }
  176. // This order (and the avoidance of yields) is important to make
  177. // sure that when publish functions are rerun, they see a
  178. // consistent view of the world: the userId is set and matches
  179. // the login token on the connection (not that there is
  180. // currently a public API for reading the login token on a
  181. // connection).
  182. Meteor._noYieldsAllowed(function () {
  183. Accounts._setLoginToken(
  184. userId,
  185. methodInvocation.connection,
  186. Accounts._hashLoginToken(stampedLoginToken.token)
  187. );
  188. });
  189. methodInvocation.setUserId(userId);
  190. return {
  191. id: userId,
  192. token: stampedLoginToken.token,
  193. tokenExpires: Accounts._tokenExpiration(stampedLoginToken.when)
  194. };
  195. };
  196. // After a login method has completed, call the login hooks. Note
  197. // that `attemptLogin` is called for *all* login attempts, even ones
  198. // which aren't successful (such as an invalid password, etc).
  199. //
  200. // If the login is allowed and isn't aborted by a validate login hook
  201. // callback, log in the user.
  202. //
  203. var attemptLogin = function (methodInvocation, methodName, methodArgs, result) {
  204. if (!result)
  205. throw new Error("result is required");
  206. // XXX A programming error in a login handler can lead to this occuring, and
  207. // then we don't call onLogin or onLoginFailure callbacks. Should
  208. // tryLoginMethod catch this case and turn it into an error?
  209. if (!result.userId && !result.error)
  210. throw new Error("A login method must specify a userId or an error");
  211. var user;
  212. if (result.userId)
  213. user = Meteor.users.findOne(result.userId);
  214. var attempt = {
  215. type: result.type || "unknown",
  216. allowed: !! (result.userId && !result.error),
  217. methodName: methodName,
  218. methodArguments: _.toArray(methodArgs)
  219. };
  220. if (result.error)
  221. attempt.error = result.error;
  222. if (user)
  223. attempt.user = user;
  224. // validateLogin may mutate `attempt` by adding an error and changing allowed
  225. // to false, but that's the only change it can make (and the user's callbacks
  226. // only get a clone of `attempt`).
  227. validateLogin(methodInvocation.connection, attempt);
  228. if (attempt.allowed) {
  229. var ret = _.extend(
  230. loginUser(methodInvocation, result.userId, result.stampedLoginToken),
  231. result.options || {}
  232. );
  233. successfulLogin(methodInvocation.connection, attempt);
  234. return ret;
  235. }
  236. else {
  237. failedLogin(methodInvocation.connection, attempt);
  238. throw attempt.error;
  239. }
  240. };
  241. // All service specific login methods should go through this function.
  242. // Ensure that thrown exceptions are caught and that login hook
  243. // callbacks are still called.
  244. //
  245. Accounts._loginMethod = function (methodInvocation, methodName, methodArgs, type, fn) {
  246. return attemptLogin(
  247. methodInvocation,
  248. methodName,
  249. methodArgs,
  250. tryLoginMethod(type, fn)
  251. );
  252. };
  253. // Report a login attempt failed outside the context of a normal login
  254. // method. This is for use in the case where there is a multi-step login
  255. // procedure (eg SRP based password login). If a method early in the
  256. // chain fails, it should call this function to report a failure. There
  257. // is no corresponding method for a successful login; methods that can
  258. // succeed at logging a user in should always be actual login methods
  259. // (using either Accounts._loginMethod or Accounts.registerLoginHandler).
  260. Accounts._reportLoginFailure = function (methodInvocation, methodName, methodArgs, result) {
  261. var attempt = {
  262. type: result.type || "unknown",
  263. allowed: false,
  264. error: result.error,
  265. methodName: methodName,
  266. methodArguments: _.toArray(methodArgs)
  267. };
  268. if (result.userId)
  269. attempt.user = Meteor.users.findOne(result.userId);
  270. validateLogin(methodInvocation.connection, attempt);
  271. failedLogin(methodInvocation.connection, attempt);
  272. };
  273. ///
  274. /// LOGIN HANDLERS
  275. ///
  276. // list of all registered handlers.
  277. var loginHandlers = [];
  278. // The main entry point for auth packages to hook in to login.
  279. //
  280. // A login handler is a login method which can return `undefined` to
  281. // indicate that the login request is not handled by this handler.
  282. //
  283. // @param name {String} Optional. The service name, used by default
  284. // if a specific service name isn't returned in the result.
  285. //
  286. // @param handler {Function} A function that receives an options object
  287. // (as passed as an argument to the `login` method) and returns one of:
  288. // - `undefined`, meaning don't handle;
  289. // - a login method result object
  290. Accounts.registerLoginHandler = function(name, handler) {
  291. if (! handler) {
  292. handler = name;
  293. name = null;
  294. }
  295. loginHandlers.push({name: name, handler: handler});
  296. };
  297. // Checks a user's credentials against all the registered login
  298. // handlers, and returns a login token if the credentials are valid. It
  299. // is like the login method, except that it doesn't set the logged-in
  300. // user on the connection. Throws a Meteor.Error if logging in fails,
  301. // including the case where none of the login handlers handled the login
  302. // request. Otherwise, returns {id: userId, token: *, tokenExpires: *}.
  303. //
  304. // For example, if you want to login with a plaintext password, `options` could be
  305. // { user: { username: <username> }, password: <password> }, or
  306. // { user: { email: <email> }, password: <password> }.
  307. // Try all of the registered login handlers until one of them doesn't
  308. // return `undefined`, meaning it handled this call to `login`. Return
  309. // that return value.
  310. var runLoginHandlers = function (methodInvocation, options) {
  311. for (var i = 0; i < loginHandlers.length; ++i) {
  312. var handler = loginHandlers[i];
  313. var result = tryLoginMethod(
  314. handler.name,
  315. function () {
  316. return handler.handler.call(methodInvocation, options);
  317. }
  318. );
  319. if (result)
  320. return result;
  321. else if (result !== undefined)
  322. throw new Meteor.Error(400, "A login handler should return a result or undefined");
  323. }
  324. return {
  325. type: null,
  326. error: new Meteor.Error(400, "Unrecognized options for login request")
  327. };
  328. };
  329. // Deletes the given loginToken from the database.
  330. //
  331. // For new-style hashed token, this will cause all connections
  332. // associated with the token to be closed.
  333. //
  334. // Any connections associated with old-style unhashed tokens will be
  335. // in the process of becoming associated with hashed tokens and then
  336. // they'll get closed.
  337. Accounts.destroyToken = function (userId, loginToken) {
  338. Meteor.users.update(userId, {
  339. $pull: {
  340. "services.resume.loginTokens": {
  341. $or: [
  342. { hashedToken: loginToken },
  343. { token: loginToken }
  344. ]
  345. }
  346. }
  347. });
  348. };
  349. // Actual methods for login and logout. This is the entry point for
  350. // clients to actually log in.
  351. Meteor.methods({
  352. // @returns {Object|null}
  353. // If successful, returns {token: reconnectToken, id: userId}
  354. // If unsuccessful (for example, if the user closed the oauth login popup),
  355. // throws an error describing the reason
  356. login: function(options) {
  357. var self = this;
  358. // Login handlers should really also check whatever field they look at in
  359. // options, but we don't enforce it.
  360. check(options, Object);
  361. var result = runLoginHandlers(self, options);
  362. return attemptLogin(self, "login", arguments, result);
  363. },
  364. logout: function() {
  365. var token = Accounts._getLoginToken(this.connection.id);
  366. Accounts._setLoginToken(this.userId, this.connection, null);
  367. if (token && this.userId)
  368. Accounts.destroyToken(this.userId, token);
  369. this.setUserId(null);
  370. },
  371. // Delete all the current user's tokens and close all open connections logged
  372. // in as this user. Returns a fresh new login token that this client can
  373. // use. Tests set Accounts._noConnectionCloseDelayForTest to delete tokens
  374. // immediately instead of using a delay.
  375. //
  376. // @returns {Object} Object with token and tokenExpires keys.
  377. logoutOtherClients: function () {
  378. var self = this;
  379. var user = Meteor.users.findOne(self.userId, {
  380. fields: {
  381. "services.resume.loginTokens": true
  382. }
  383. });
  384. if (user) {
  385. // Save the current tokens in the database to be deleted in
  386. // CONNECTION_CLOSE_DELAY_MS ms. This gives other connections in the
  387. // caller's browser time to find the fresh token in localStorage. We save
  388. // the tokens in the database in case we crash before actually deleting
  389. // them.
  390. var tokens = user.services.resume.loginTokens;
  391. var newToken = Accounts._generateStampedLoginToken();
  392. var userId = self.userId;
  393. Meteor.users.update(self.userId, {
  394. $set: {
  395. "services.resume.loginTokensToDelete": tokens,
  396. "services.resume.haveLoginTokensToDelete": true
  397. },
  398. $push: { "services.resume.loginTokens": Accounts._hashStampedToken(newToken) }
  399. });
  400. Meteor.setTimeout(function () {
  401. // The observe on Meteor.users will take care of closing the connections
  402. // associated with `tokens`.
  403. deleteSavedTokens(userId, tokens);
  404. }, Accounts._noConnectionCloseDelayForTest ? 0 :
  405. CONNECTION_CLOSE_DELAY_MS);
  406. // We do not set the login token on this connection, but instead the
  407. // observe closes the connection and the client will reconnect with the
  408. // new token.
  409. return {
  410. token: newToken.token,
  411. tokenExpires: Accounts._tokenExpiration(newToken.when)
  412. };
  413. } else {
  414. throw new Error("You are not logged in.");
  415. }
  416. }
  417. });
  418. ///
  419. /// ACCOUNT DATA
  420. ///
  421. // connectionId -> {connection, loginToken, srpChallenge}
  422. var accountData = {};
  423. // HACK: This is used by 'meteor-accounts' to get the loginToken for a
  424. // connection. Maybe there should be a public way to do that.
  425. Accounts._getAccountData = function (connectionId, field) {
  426. var data = accountData[connectionId];
  427. return data && data[field];
  428. };
  429. Accounts._setAccountData = function (connectionId, field, value) {
  430. var data = accountData[connectionId];
  431. // safety belt. shouldn't happen. accountData is set in onConnection,
  432. // we don't have a connectionId until it is set.
  433. if (!data)
  434. return;
  435. if (value === undefined)
  436. delete data[field];
  437. else
  438. data[field] = value;
  439. };
  440. Meteor.server.onConnection(function (connection) {
  441. accountData[connection.id] = {connection: connection};
  442. connection.onClose(function () {
  443. removeTokenFromConnection(connection.id);
  444. delete accountData[connection.id];
  445. });
  446. });
  447. ///
  448. /// RECONNECT TOKENS
  449. ///
  450. /// support reconnecting using a meteor login token
  451. Accounts._hashLoginToken = function (loginToken) {
  452. var hash = crypto.createHash('sha256');
  453. hash.update(loginToken);
  454. return hash.digest('base64');
  455. };
  456. // {token, when} => {hashedToken, when}
  457. Accounts._hashStampedToken = function (stampedToken) {
  458. return _.extend(
  459. _.omit(stampedToken, 'token'),
  460. {hashedToken: Accounts._hashLoginToken(stampedToken.token)}
  461. );
  462. };
  463. // Using $addToSet avoids getting an index error if another client
  464. // logging in simultaneously has already inserted the new hashed
  465. // token.
  466. Accounts._insertHashedLoginToken = function (userId, hashedToken, query) {
  467. query = query ? _.clone(query) : {};
  468. query._id = userId;
  469. Meteor.users.update(
  470. query,
  471. { $addToSet: {
  472. "services.resume.loginTokens": hashedToken
  473. } }
  474. );
  475. };
  476. // Exported for tests.
  477. Accounts._insertLoginToken = function (userId, stampedToken, query) {
  478. Accounts._insertHashedLoginToken(
  479. userId,
  480. Accounts._hashStampedToken(stampedToken),
  481. query
  482. );
  483. };
  484. Accounts._clearAllLoginTokens = function (userId) {
  485. Meteor.users.update(
  486. userId,
  487. {$set: {'services.resume.loginTokens': []}}
  488. );
  489. };
  490. // connection id -> observe handle for the login token that this
  491. // connection is currently associated with, or null. Null indicates that
  492. // we are in the process of setting up the observe.
  493. var userObservesForConnections = {};
  494. // test hook
  495. Accounts._getUserObserve = function (connectionId) {
  496. return userObservesForConnections[connectionId];
  497. };
  498. // Clean up this connection's association with the token: that is, stop
  499. // the observe that we started when we associated the connection with
  500. // this token.
  501. var removeTokenFromConnection = function (connectionId) {
  502. var observe = userObservesForConnections[connectionId];
  503. if (observe !== undefined) {
  504. if (observe === null) {
  505. // We're in the process of setting up an observe for this
  506. // connection. We can't clean up that observe yet, but if we
  507. // delete the null placeholder for this connection, then the
  508. // observe will get cleaned up as soon as it has been set up.
  509. delete userObservesForConnections[connectionId];
  510. } else {
  511. delete userObservesForConnections[connectionId];
  512. observe.stop();
  513. }
  514. }
  515. };
  516. Accounts._getLoginToken = function (connectionId) {
  517. return Accounts._getAccountData(connectionId, 'loginToken');
  518. };
  519. // newToken is a hashed token.
  520. Accounts._setLoginToken = function (userId, connection, newToken) {
  521. removeTokenFromConnection(connection.id);
  522. Accounts._setAccountData(connection.id, 'loginToken', newToken);
  523. if (newToken) {
  524. // Set up an observe for this token. If the token goes away, we need
  525. // to close the connection. We defer the observe because there's
  526. // no need for it to be on the critical path for login; we just need
  527. // to ensure that the connection will get closed at some point if
  528. // the token gets deleted.
  529. //
  530. // Initially, we set the observe for this connection to null; this
  531. // signifies to other code (which might run while we yield) that we
  532. // are in the process of setting up an observe for this
  533. // connection. Once the observe is ready to go, we replace null with
  534. // the real observe handle (unless the placeholder has been deleted,
  535. // signifying that the connection was closed already -- in this case
  536. // we just clean up the observe that we started).
  537. userObservesForConnections[connection.id] = null;
  538. Meteor.defer(function () {
  539. var foundMatchingUser;
  540. // Because we upgrade unhashed login tokens to hashed tokens at
  541. // login time, sessions will only be logged in with a hashed
  542. // token. Thus we only need to observe hashed tokens here.
  543. var observe = Meteor.users.find({
  544. _id: userId,
  545. 'services.resume.loginTokens.hashedToken': newToken
  546. }, { fields: { _id: 1 } }).observeChanges({
  547. added: function () {
  548. foundMatchingUser = true;
  549. },
  550. removed: function () {
  551. connection.close();
  552. // The onClose callback for the connection takes care of
  553. // cleaning up the observe handle and any other state we have
  554. // lying around.
  555. }
  556. });
  557. if (_.has(userObservesForConnections, connection.id)) {
  558. if (userObservesForConnections[connection.id] !== null) {
  559. throw new Error("Non-null user observe for connection " +
  560. connection.id + " while observe was being set up?");
  561. }
  562. userObservesForConnections[connection.id] = observe;
  563. } else {
  564. // Oops, this connection was closed while we were setting up the
  565. // observe. Clean it up now.
  566. observe.stop();
  567. }
  568. if (! foundMatchingUser) {
  569. // We've set up an observe on the user associated with `newToken`,
  570. // so if the new token is removed from the database, we'll close
  571. // the connection. But the token might have already been deleted
  572. // before we set up the observe, which wouldn't have closed the
  573. // connection because the observe wasn't running yet.
  574. connection.close();
  575. }
  576. });
  577. }
  578. };
  579. // Login handler for resume tokens.
  580. Accounts.registerLoginHandler("resume", function(options) {
  581. if (!options.resume)
  582. return undefined;
  583. check(options.resume, String);
  584. var hashedToken = Accounts._hashLoginToken(options.resume);
  585. // First look for just the new-style hashed login token, to avoid
  586. // sending the unhashed token to the database in a query if we don't
  587. // need to.
  588. var user = Meteor.users.findOne(
  589. {"services.resume.loginTokens.hashedToken": hashedToken});
  590. if (! user) {
  591. // If we didn't find the hashed login token, try also looking for
  592. // the old-style unhashed token. But we need to look for either
  593. // the old-style token OR the new-style token, because another
  594. // client connection logging in simultaneously might have already
  595. // converted the token.
  596. user = Meteor.users.findOne({
  597. $or: [
  598. {"services.resume.loginTokens.hashedToken": hashedToken},
  599. {"services.resume.loginTokens.token": options.resume}
  600. ]
  601. });
  602. }
  603. if (! user)
  604. return {
  605. error: new Meteor.Error(403, "You've been logged out by the server. Please log in again.")
  606. };
  607. // Find the token, which will either be an object with fields
  608. // {hashedToken, when} for a hashed token or {token, when} for an
  609. // unhashed token.
  610. var oldUnhashedStyleToken;
  611. var token = _.find(user.services.resume.loginTokens, function (token) {
  612. return token.hashedToken === hashedToken;
  613. });
  614. if (token) {
  615. oldUnhashedStyleToken = false;
  616. } else {
  617. token = _.find(user.services.resume.loginTokens, function (token) {
  618. return token.token === options.resume;
  619. });
  620. oldUnhashedStyleToken = true;
  621. }
  622. var tokenExpires = Accounts._tokenExpiration(token.when);
  623. if (new Date() >= tokenExpires)
  624. return {
  625. userId: user._id,
  626. error: new Meteor.Error(403, "Your session has expired. Please log in again.")
  627. };
  628. // Update to a hashed token when an unhashed token is encountered.
  629. if (oldUnhashedStyleToken) {
  630. // Only add the new hashed token if the old unhashed token still
  631. // exists (this avoids resurrecting the token if it was deleted
  632. // after we read it). Using $addToSet avoids getting an index
  633. // error if another client logging in simultaneously has already
  634. // inserted the new hashed token.
  635. Meteor.users.update(
  636. {
  637. _id: user._id,
  638. "services.resume.loginTokens.token": options.resume
  639. },
  640. {$addToSet: {
  641. "services.resume.loginTokens": {
  642. "hashedToken": hashedToken,
  643. "when": token.when
  644. }
  645. }}
  646. );
  647. // Remove the old token *after* adding the new, since otherwise
  648. // another client trying to login between our removing the old and
  649. // adding the new wouldn't find a token to login with.
  650. Meteor.users.update(user._id, {
  651. $pull: {
  652. "services.resume.loginTokens": { "token": options.resume }
  653. }
  654. });
  655. }
  656. return {
  657. userId: user._id,
  658. stampedLoginToken: {
  659. token: options.resume,
  660. when: token.when
  661. }
  662. };
  663. });
  664. // (Also used by Meteor Accounts server and tests).
  665. //
  666. Accounts._generateStampedLoginToken = function () {
  667. return {token: Random.id(), when: (new Date)};
  668. };
  669. ///
  670. /// TOKEN EXPIRATION
  671. ///
  672. var expireTokenInterval;
  673. // Deletes expired tokens from the database and closes all open connections
  674. // associated with these tokens.
  675. //
  676. // Exported for tests. Also, the arguments are only used by
  677. // tests. oldestValidDate is simulate expiring tokens without waiting
  678. // for them to actually expire. userId is used by tests to only expire
  679. // tokens for the test user.
  680. var expireTokens = Accounts._expireTokens = function (oldestValidDate, userId) {
  681. var tokenLifetimeMs = getTokenLifetimeMs();
  682. // when calling from a test with extra arguments, you must specify both!
  683. if ((oldestValidDate && !userId) || (!oldestValidDate && userId)) {
  684. throw new Error("Bad test. Must specify both oldestValidDate and userId.");
  685. }
  686. oldestValidDate = oldestValidDate ||
  687. (new Date(new Date() - tokenLifetimeMs));
  688. var userFilter = userId ? {_id: userId} : {};
  689. // Backwards compatible with older versions of meteor that stored login token
  690. // timestamps as numbers.
  691. Meteor.users.update(_.extend(userFilter, {
  692. $or: [
  693. { "services.resume.loginTokens.when": { $lt: oldestValidDate } },
  694. { "services.resume.loginTokens.when": { $lt: +oldestValidDate } }
  695. ]
  696. }), {
  697. $pull: {
  698. "services.resume.loginTokens": {
  699. $or: [
  700. { when: { $lt: oldestValidDate } },
  701. { when: { $lt: +oldestValidDate } }
  702. ]
  703. }
  704. }
  705. }, { multi: true });
  706. // The observe on Meteor.users will take care of closing connections for
  707. // expired tokens.
  708. };
  709. maybeStopExpireTokensInterval = function () {
  710. if (_.has(Accounts._options, "loginExpirationInDays") &&
  711. Accounts._options.loginExpirationInDays === null &&
  712. expireTokenInterval) {
  713. Meteor.clearInterval(expireTokenInterval);
  714. expireTokenInterval = null;
  715. }
  716. };
  717. expireTokenInterval = Meteor.setInterval(expireTokens,
  718. EXPIRE_TOKENS_INTERVAL_MS);
  719. ///
  720. /// CREATE USER HOOKS
  721. ///
  722. var onCreateUserHook = null;
  723. Accounts.onCreateUser = function (func) {
  724. if (onCreateUserHook)
  725. throw new Error("Can only call onCreateUser once");
  726. else
  727. onCreateUserHook = func;
  728. };
  729. // XXX see comment on Accounts.createUser in passwords_server about adding a
  730. // second "server options" argument.
  731. var defaultCreateUserHook = function (options, user) {
  732. if (options.profile)
  733. user.profile = options.profile;
  734. return user;
  735. };
  736. // Called by accounts-password
  737. Accounts.insertUserDoc = function (options, user) {
  738. // - clone user document, to protect from modification
  739. // - add createdAt timestamp
  740. // - prepare an _id, so that you can modify other collections (eg
  741. // create a first task for every new user)
  742. //
  743. // XXX If the onCreateUser or validateNewUser hooks fail, we might
  744. // end up having modified some other collection
  745. // inappropriately. The solution is probably to have onCreateUser
  746. // accept two callbacks - one that gets called before inserting
  747. // the user document (in which you can modify its contents), and
  748. // one that gets called after (in which you should change other
  749. // collections)
  750. user = _.extend({createdAt: new Date(), _id: Random.id()}, user);
  751. var fullUser;
  752. if (onCreateUserHook) {
  753. fullUser = onCreateUserHook(options, user);
  754. // This is *not* part of the API. We need this because we can't isolate
  755. // the global server environment between tests, meaning we can't test
  756. // both having a create user hook set and not having one set.
  757. if (fullUser === 'TEST DEFAULT HOOK')
  758. fullUser = defaultCreateUserHook(options, user);
  759. } else {
  760. fullUser = defaultCreateUserHook(options, user);
  761. }
  762. _.each(validateNewUserHooks, function (hook) {
  763. if (!hook(fullUser))
  764. throw new Meteor.Error(403, "User validation failed");
  765. });
  766. var userId;
  767. try {
  768. userId = Meteor.users.insert(fullUser);
  769. } catch (e) {
  770. // XXX string parsing sucks, maybe
  771. // https://jira.mongodb.org/browse/SERVER-3069 will get fixed one day
  772. if (e.name !== 'MongoError') throw e;
  773. var match = e.err.match(/^E11000 duplicate key error index: ([^ ]+)/);
  774. if (!match) throw e;
  775. if (match[1].indexOf('$emails.address') !== -1)
  776. throw new Meteor.Error(403, "Email already exists.");
  777. if (match[1].indexOf('username') !== -1)
  778. throw new Meteor.Error(403, "Username already exists.");
  779. // XXX better error reporting for services.facebook.id duplicate, etc
  780. throw e;
  781. }
  782. return userId;
  783. };
  784. var validateNewUserHooks = [];
  785. Accounts.validateNewUser = function (func) {
  786. validateNewUserHooks.push(func);
  787. };
  788. // XXX Find a better place for this utility function
  789. // Like Perl's quotemeta: quotes all regexp metacharacters. See
  790. // https://github.com/substack/quotemeta/blob/master/index.js
  791. var quotemeta = function (str) {
  792. return String(str).replace(/(\W)/g, '\\$1');
  793. };
  794. // Helper function: returns false if email does not match company domain from
  795. // the configuration.
  796. var testEmailDomain = function (email) {
  797. var domain = Accounts._options.restrictCreationByEmailDomain;
  798. return !domain ||
  799. (_.isFunction(domain) && domain(email)) ||
  800. (_.isString(domain) &&
  801. (new RegExp('@' + quotemeta(domain) + '$', 'i')).test(email));
  802. };
  803. // Validate new user's email or Google/Facebook/GitHub account's email
  804. Accounts.validateNewUser(function (user) {
  805. var domain = Accounts._options.restrictCreationByEmailDomain;
  806. if (!domain)
  807. return true;
  808. var emailIsGood = false;
  809. if (!_.isEmpty(user.emails)) {
  810. emailIsGood = _.any(user.emails, function (email) {
  811. return testEmailDomain(email.address);
  812. });
  813. } else if (!_.isEmpty(user.services)) {
  814. // Find any email of any service and check it
  815. emailIsGood = _.any(user.services, function (service) {
  816. return service.email && testEmailDomain(service.email);
  817. });
  818. }
  819. if (emailIsGood)
  820. return true;
  821. if (_.isString(domain))
  822. throw new Meteor.Error(403, "@" + domain + " email required");
  823. else
  824. throw new Meteor.Error(403, "Email doesn't match the criteria.");
  825. });
  826. ///
  827. /// MANAGING USER OBJECTS
  828. ///
  829. // Updates or creates a user after we authenticate with a 3rd party.
  830. //
  831. // @param serviceName {String} Service name (eg, twitter).
  832. // @param serviceData {Object} Data to store in the user's record
  833. // under services[serviceName]. Must include an "id" field
  834. // which is a unique identifier for the user in the service.
  835. // @param options {Object, optional} Other options to pass to insertUserDoc
  836. // (eg, profile)
  837. // @returns {Object} Object with token and id keys, like the result
  838. // of the "login" method.
  839. //
  840. Accounts.updateOrCreateUserFromExternalService = function(
  841. serviceName, serviceData, options) {
  842. options = _.clone(options || {});
  843. if (serviceName === "password" || serviceName === "resume")
  844. throw new Error(
  845. "Can't use updateOrCreateUserFromExternalService with internal service "
  846. + serviceName);
  847. if (!_.has(serviceData, 'id'))
  848. throw new Error(
  849. "Service data for service " + serviceName + " must include id");
  850. // Look for a user with the appropriate service user id.
  851. var selector = {};
  852. var serviceIdKey = "services." + serviceName + ".id";
  853. // XXX Temporary special case for Twitter. (Issue #629)
  854. // The serviceData.id will be a string representation of an integer.
  855. // We want it to match either a stored string or int representation.
  856. // This is to cater to earlier versions of Meteor storing twitter
  857. // user IDs in number form, and recent versions storing them as strings.
  858. // This can be removed once migration technology is in place, and twitter
  859. // users stored with integer IDs have been migrated to string IDs.
  860. if (serviceName === "twitter" && !isNaN(serviceData.id)) {
  861. selector["$or"] = [{},{}];
  862. selector["$or"][0][serviceIdKey] = serviceData.id;
  863. selector["$or"][1][serviceIdKey] = parseInt(serviceData.id, 10);
  864. } else {
  865. selector[serviceIdKey] = serviceData.id;
  866. }
  867. var user = Meteor.users.findOne(selector);
  868. if (user) {
  869. // We *don't* process options (eg, profile) for update, but we do replace
  870. // the serviceData (eg, so that we keep an unexpired access token and
  871. // don't cache old email addresses in serviceData.email).
  872. // XXX provide an onUpdateUser hook which would let apps update
  873. // the profile too
  874. var setAttrs = {};
  875. _.each(serviceData, function(value, key) {
  876. setAttrs["services." + serviceName + "." + key] = value;
  877. });
  878. // XXX Maybe we should re-use the selector above and notice if the update
  879. // touches nothing?
  880. Meteor.users.update(user._id, {$set: setAttrs});
  881. return {
  882. type: serviceName,
  883. userId: user._id
  884. };
  885. } else {
  886. // Create a new user with the service data. Pass other options through to
  887. // insertUserDoc.
  888. user = {services: {}};
  889. user.services[serviceName] = serviceData;
  890. return {
  891. type: serviceName,
  892. userId: Accounts.insertUserDoc(options, user)
  893. };
  894. }
  895. };
  896. ///
  897. /// PUBLISHING DATA
  898. ///
  899. // Publish the current user's record to the client.
  900. Meteor.publish(null, function() {
  901. if (this.userId) {
  902. return Meteor.users.find(
  903. {_id: this.userId},
  904. {fields: {profile: 1, username: 1, emails: 1}});
  905. } else {
  906. return null;
  907. }
  908. }, /*suppress autopublish warning*/{is_auto: true});
  909. // If autopublish is on, publish these user fields. Login service
  910. // packages (eg accounts-google) add to these by calling
  911. // Accounts.addAutopublishFields Notably, this isn't implemented with
  912. // multiple publishes since DDP only merges only across top-level
  913. // fields, not subfields (such as 'services.facebook.accessToken')
  914. var autopublishFields = {
  915. loggedInUser: ['profile', 'username', 'emails'],
  916. otherUsers: ['profile', 'username']
  917. };
  918. // Add to the list of fields or subfields to be automatically
  919. // published if autopublish is on. Must be called from top-level
  920. // code (ie, before Meteor.startup hooks run).
  921. //
  922. // @param opts {Object} with:
  923. // - forLoggedInUser {Array} Array of fields published to the logged-in user
  924. // - forOtherUsers {Array} Array of fields published to users that aren't logged in
  925. Accounts.addAutopublishFields = function(opts) {
  926. autopublishFields.loggedInUser.push.apply(
  927. autopublishFields.loggedInUser, opts.forLoggedInUser);
  928. autopublishFields.otherUsers.push.apply(
  929. autopublishFields.otherUsers, opts.forOtherUsers);
  930. };
  931. if (Package.autopublish) {
  932. // Use Meteor.startup to give other packages a chance to call
  933. // addAutopublishFields.
  934. Meteor.startup(function () {
  935. // ['profile', 'username'] -> {profile: 1, username: 1}
  936. var toFieldSelector = function(fields) {
  937. return _.object(_.map(fields, function(field) {
  938. return [field, 1];
  939. }));
  940. };
  941. Meteor.server.publish(null, function () {
  942. if (this.userId) {
  943. return Meteor.users.find(
  944. {_id: this.userId},
  945. {fields: toFieldSelector(autopublishFields.loggedInUser)});
  946. } else {
  947. return null;
  948. }
  949. }, /*suppress autopublish warning*/{is_auto: true});
  950. // XXX this publish is neither dedup-able nor is it optimized by our special
  951. // treatment of queries on a specific _id. Therefore this will have O(n^2)
  952. // run-time performance every time a user document is changed (eg someone
  953. // logging in). If this is a problem, we can instead write a manual publish
  954. // function which filters out fields based on 'this.userId'.
  955. Meteor.server.publish(null, function () {
  956. var selector;
  957. if (this.userId)
  958. selector = {_id: {$ne: this.userId}};
  959. else
  960. selector = {};
  961. return Meteor.users.find(
  962. selector,
  963. {fields: toFieldSelector(autopublishFields.otherUsers)});
  964. }, /*suppress autopublish warning*/{is_auto: true});
  965. });
  966. }
  967. // Publish all login service configuration fields other than secret.
  968. Meteor.publish("meteor.loginServiceConfiguration", function () {
  969. var ServiceConfiguration =
  970. Package['service-configuration'].ServiceConfiguration;
  971. return ServiceConfiguration.configurations.find({}, {fields: {secret: 0}});
  972. }, {is_auto: true}); // not techincally autopublish, but stops the warning.
  973. // Allow a one-time configuration for a login service. Modifications
  974. // to this collection are also allowed in insecure mode.
  975. Meteor.methods({
  976. "configureLoginService": function (options) {
  977. check(options, Match.ObjectIncluding({service: String}));
  978. // Don't let random users configure a service we haven't added yet (so
  979. // that when we do later add it, it's set up with their configuration
  980. // instead of ours).
  981. // XXX if service configuration is oauth-specific then this code should
  982. // be in accounts-oauth; if it's not then the registry should be
  983. // in this package
  984. if (!(Accounts.oauth
  985. && _.contains(Accounts.oauth.serviceNames(), options.service))) {
  986. throw new Meteor.Error(403, "Service unknown");
  987. }
  988. var ServiceConfiguration =
  989. Package['service-configuration'].ServiceConfiguration;
  990. if (ServiceConfiguration.configurations.findOne({service: options.service}))
  991. throw new Meteor.Error(403, "Service " + options.service + " already configured");
  992. ServiceConfiguration.configurations.insert(options);
  993. }
  994. });
  995. ///
  996. /// RESTRICTING WRITES TO USER OBJECTS
  997. ///
  998. Meteor.users.allow({
  999. // clients can modify the profile field of their own document, and
  1000. // nothing else.
  1001. update: function (userId, user, fields, modifier) {
  1002. // make sure it is our record
  1003. if (user._id !== userId)
  1004. return false;
  1005. // user can only modify the 'profile' field. sets to multiple
  1006. // sub-keys (eg profile.foo and profile.bar) are merged into entry
  1007. // in the fields list.
  1008. if (fields.length !== 1 || fields[0] !== 'profile')
  1009. return false;
  1010. return true;
  1011. },
  1012. fetch: ['_id'] // we only look at _id.
  1013. });
  1014. /// DEFAULT INDEXES ON USERS
  1015. Meteor.users._ensureIndex('username', {unique: 1, sparse: 1});
  1016. Meteor.users._ensureIndex('emails.address', {unique: 1, sparse: 1});
  1017. Meteor.users._ensureIndex('services.resume.loginTokens.hashedToken',
  1018. {unique: 1, sparse: 1});
  1019. Meteor.users._ensureIndex('services.resume.loginTokens.token',
  1020. {unique: 1, sparse: 1});
  1021. // For taking care of logoutOtherClients calls that crashed before the tokens
  1022. // were deleted.
  1023. Meteor.users._ensureIndex('services.resume.haveLoginTokensToDelete',
  1024. { sparse: 1 });
  1025. // For expiring login tokens
  1026. Meteor.users._ensureIndex("services.resume.loginTokens.when", { sparse: 1 });
  1027. ///
  1028. /// CLEAN UP FOR `logoutOtherClients`
  1029. ///
  1030. var deleteSavedTokens = function (userId, tokensToDelete) {
  1031. if (tokensToDelete) {
  1032. Meteor.users.update(userId, {
  1033. $unset: {
  1034. "services.resume.haveLoginTokensToDelete": 1,
  1035. "services.resume.loginTokensToDelete": 1
  1036. },
  1037. $pullAll: {
  1038. "services.resume.loginTokens": tokensToDelete
  1039. }
  1040. });
  1041. }
  1042. };
  1043. Meteor.startup(function () {
  1044. // If we find users who have saved tokens to delete on startup, delete them
  1045. // now. It's possible that the server could have crashed and come back up
  1046. // before new tokens are found in localStorage, but this shouldn't happen very
  1047. // often. We shouldn't put a delay here because that would give a lot of power
  1048. // to an attacker with a stolen login token and the ability to crash the
  1049. // server.
  1050. var users = Meteor.users.find({
  1051. "services.resume.haveLoginTokensToDelete": true
  1052. }, {
  1053. "services.resume.loginTokensToDelete": 1
  1054. });
  1055. users.forEach(function (user) {
  1056. deleteSavedTokens(user._id, user.services.resume.loginTokensToDelete);
  1057. });
  1058. });