1
0
Fork 0
mirror of https://github.com/YunoHost-Apps/rocketchat_ynh.git synced 2024-09-03 20:16:25 +02:00
rocketchat_ynh/sources/programs/server/packages/accounts-base.js
2016-04-29 16:32:48 +02:00

1827 lines
206 KiB
JavaScript

(function () {
/* Imports */
var Meteor = Package.meteor.Meteor;
var _ = Package.underscore._;
var ECMAScript = Package.ecmascript.ECMAScript;
var DDPRateLimiter = Package['ddp-rate-limiter'].DDPRateLimiter;
var check = Package.check.check;
var Match = Package.check.Match;
var Random = Package.random.Random;
var EJSON = Package.ejson.EJSON;
var Hook = Package['callback-hook'].Hook;
var DDP = Package['ddp-client'].DDP;
var DDPServer = Package['ddp-server'].DDPServer;
var MongoInternals = Package.mongo.MongoInternals;
var Mongo = Package.mongo.Mongo;
var babelHelpers = Package['babel-runtime'].babelHelpers;
var Symbol = Package['ecmascript-runtime'].Symbol;
var Map = Package['ecmascript-runtime'].Map;
var Set = Package['ecmascript-runtime'].Set;
var Promise = Package.promise.Promise;
/* Package-scope variables */
var AccountsCommon, EXPIRE_TOKENS_INTERVAL_MS, CONNECTION_CLOSE_DELAY_MS, AccountsServer, Accounts, AccountsTest;
(function(){
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/accounts-base/accounts_common.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
/** //
* @summary Super-constructor for AccountsClient and AccountsServer. //
* @locus Anywhere //
* @class AccountsCommon //
* @instancename accountsClientOrServer //
* @param options {Object} an object with fields: //
* - connection {Object} Optional DDP connection to reuse. //
* - ddpUrl {String} Optional URL for creating a new DDP connection. //
*/ //
AccountsCommon = (function () { // 10
function AccountsCommon(options) { // 11
babelHelpers.classCallCheck(this, AccountsCommon); //
//
// Currently this is read directly by packages like accounts-password //
// and accounts-ui-unstyled. //
this._options = {}; // 14
//
// Note that setting this.connection = null causes this.users to be a //
// LocalCollection, which is not what we want. //
this.connection = undefined; // 18
this._initConnection(options || {}); // 19
//
// There is an allow call in accounts_server.js that restricts writes to //
// this collection. //
this.users = new Mongo.Collection("users", { // 23
_preventAutopublish: true, // 24
connection: this.connection // 25
}); //
//
// Callback exceptions are printed with Meteor._debug and ignored. //
this._onLoginHook = new Hook({ // 29
bindEnvironment: false, // 30
debugPrintExceptions: "onLogin callback" // 31
}); //
//
this._onLoginFailureHook = new Hook({ // 34
bindEnvironment: false, // 35
debugPrintExceptions: "onLoginFailure callback" // 36
}); //
} //
//
/** //
* @summary Get the current user id, or `null` if no user is logged in. A reactive data source. //
* @locus Anywhere but publish functions //
*/ //
//
AccountsCommon.prototype.userId = (function () { // 10
function userId() { // 44
throw new Error("userId method not implemented"); // 45
} //
//
return userId; //
})(); //
//
/** //
* @summary Get the current user record, or `null` if no user is logged in. A reactive data source. //
* @locus Anywhere but publish functions //
*/ //
//
AccountsCommon.prototype.user = (function () { // 10
function user() { // 52
var userId = this.userId(); // 53
return userId ? this.users.findOne(userId) : null; // 54
} //
//
return user; //
})(); //
//
// Set up config for the accounts system. Call this on both the client //
// and the server. //
// //
// Note that this method gets overridden on AccountsServer.prototype, but //
// the overriding method calls the overridden method. //
// //
// XXX we should add some enforcement that this is called on both the //
// client and the server. Otherwise, a user can //
// 'forbidClientAccountCreation' only on the client and while it looks //
// like their app is secure, the server will still accept createUser //
// calls. https://github.com/meteor/meteor/issues/828 //
// //
// @param options {Object} an object with fields: //
// - sendVerificationEmail {Boolean} //
// Send email address verification emails to new users created from //
// client signups. //
// - forbidClientAccountCreation {Boolean} //
// Do not allow clients to create accounts directly. //
// - restrictCreationByEmailDomain {Function or String} //
// Require created users to have an email matching the function or //
// having the string as domain. //
// - loginExpirationInDays {Number} //
// Number of days since login until a user is logged out (login token //
// expires). //
//
/** //
* @summary Set global accounts options. //
* @locus Anywhere //
* @param {Object} options //
* @param {Boolean} options.sendVerificationEmail New users with an email address will receive an address verification email.
* @param {Boolean} options.forbidClientAccountCreation Calls to [`createUser`](#accounts_createuser) from the client will be rejected. In addition, if you are using [accounts-ui](#accountsui), the "Create account" link will not be available.
* @param {String | Function} options.restrictCreationByEmailDomain If set to a string, only allows new users if the domain part of their email address matches the string. If set to a function, only allows new users if the function returns true. The function is passed the full email address of the proposed new user. Works with password-based sign-in and external services that expose email addresses (Google, Facebook, GitHub). All existing users still can log in after enabling this option. Example: `Accounts.config({ restrictCreationByEmailDomain: 'school.edu' })`.
* @param {Number} options.loginExpirationInDays The number of days from when a user logs in until their token expires and they are logged out. Defaults to 90. Set to `null` to disable login expiration.
* @param {String} options.oauthSecretKey When using the `oauth-encryption` package, the 16 byte key using to encrypt sensitive account credentials in the database, encoded in base64. This option may only be specifed on the server. See packages/oauth-encryption/README.md for details.
*/ //
//
AccountsCommon.prototype.config = (function () { // 10
function config(options) { // 92
var self = this; // 93
//
// We don't want users to accidentally only call Accounts.config on the //
// client, where some of the options will have partial effects (eg removing //
// the "create account" button from accounts-ui if forbidClientAccountCreation //
// is set, or redirecting Google login to a specific-domain page) without //
// having their full effects. //
if (Meteor.isServer) { // 100
__meteor_runtime_config__.accountsConfigCalled = true; // 101
} else if (!__meteor_runtime_config__.accountsConfigCalled) { //
// XXX would be nice to "crash" the client and replace the UI with an error //
// message, but there's no trivial way to do this. //
Meteor._debug("Accounts.config was called on the client but not on the " + "server; some configuration options may not take effect.");
} //
//
// We need to validate the oauthSecretKey option at the time //
// Accounts.config is called. We also deliberately don't store the //
// oauthSecretKey in Accounts._options. //
if (_.has(options, "oauthSecretKey")) { // 112
if (Meteor.isClient) throw new Error("The oauthSecretKey option may only be specified on the server"); // 113
if (!Package["oauth-encryption"]) throw new Error("The oauth-encryption package must be loaded to set oauthSecretKey");
Package["oauth-encryption"].OAuthEncryption.loadKey(options.oauthSecretKey); // 117
options = _.omit(options, "oauthSecretKey"); // 118
} //
//
// validate option keys //
var VALID_KEYS = ["sendVerificationEmail", "forbidClientAccountCreation", "restrictCreationByEmailDomain", "loginExpirationInDays"];
_.each(_.keys(options), function (key) { // 124
if (!_.contains(VALID_KEYS, key)) { // 125
throw new Error("Accounts.config: Invalid key: " + key); // 126
} //
}); //
//
// set values in Accounts._options //
_.each(VALID_KEYS, function (key) { // 131
if (key in options) { // 132
if (key in self._options) { // 133
throw new Error("Can't set `" + key + "` more than once"); // 134
} //
self._options[key] = options[key]; // 136
} //
}); //
} //
//
return config; //
})(); //
//
/** //
* @summary Register a callback to be called after a login attempt succeeds. //
* @locus Anywhere //
* @param {Function} func The callback to be called when login is successful. //
*/ //
//
AccountsCommon.prototype.onLogin = (function () { // 10
function onLogin(func) { // 146
return this._onLoginHook.register(func); // 147
} //
//
return onLogin; //
})(); //
//
/** //
* @summary Register a callback to be called after a login attempt fails. //
* @locus Anywhere //
* @param {Function} func The callback to be called after the login has failed. //
*/ //
//
AccountsCommon.prototype.onLoginFailure = (function () { // 10
function onLoginFailure(func) { // 155
return this._onLoginFailureHook.register(func); // 156
} //
//
return onLoginFailure; //
})(); //
//
AccountsCommon.prototype._initConnection = (function () { // 10
function _initConnection(options) { // 159
if (!Meteor.isClient) { // 160
return; // 161
} //
//
// The connection used by the Accounts system. This is the connection //
// that will get logged in by Meteor.login(), and this is the //
// connection whose login state will be reflected by Meteor.userId(). //
// //
// It would be much preferable for this to be in accounts_client.js, //
// but it has to be here because it's needed to create the //
// Meteor.users collection. //
//
if (options.connection) { // 172
this.connection = options.connection; // 173
} else if (options.ddpUrl) { //
this.connection = DDP.connect(options.ddpUrl); // 175
} else if (typeof __meteor_runtime_config__ !== "undefined" && __meteor_runtime_config__.ACCOUNTS_CONNECTION_URL) {
// Temporary, internal hook to allow the server to point the client //
// to a different authentication server. This is for a very //
// particular use case that comes up when implementing a oauth //
// server. Unsupported and may go away at any point in time. //
// //
// We will eventually provide a general way to use account-base //
// against any DDP connection, not just one special one. //
this.connection = DDP.connect(__meteor_runtime_config__.ACCOUNTS_CONNECTION_URL); // 185
} else { //
this.connection = Meteor.connection; // 188
} //
} //
//
return _initConnection; //
})(); //
//
AccountsCommon.prototype._getTokenLifetimeMs = (function () { // 10
function _getTokenLifetimeMs() { // 192
return (this._options.loginExpirationInDays || DEFAULT_LOGIN_EXPIRATION_DAYS) * 24 * 60 * 60 * 1000; // 193
} //
//
return _getTokenLifetimeMs; //
})(); //
//
AccountsCommon.prototype._tokenExpiration = (function () { // 10
function _tokenExpiration(when) { // 197
// We pass when through the Date constructor for backwards compatibility; //
// `when` used to be a number. //
return new Date(new Date(when).getTime() + this._getTokenLifetimeMs()); // 200
} //
//
return _tokenExpiration; //
})(); //
//
AccountsCommon.prototype._tokenExpiresSoon = (function () { // 10
function _tokenExpiresSoon(when) { // 203
var minLifetimeMs = .1 * this._getTokenLifetimeMs(); // 204
var minLifetimeCapMs = MIN_TOKEN_LIFETIME_CAP_SECS * 1000; // 205
if (minLifetimeMs > minLifetimeCapMs) minLifetimeMs = minLifetimeCapMs; // 206
return new Date() > new Date(when) - minLifetimeMs; // 208
} //
//
return _tokenExpiresSoon; //
})(); //
//
return AccountsCommon; //
})(); //
//
var Ap = AccountsCommon.prototype; // 212
//
// Note that Accounts is defined separately in accounts_client.js and //
// accounts_server.js. //
//
/** //
* @summary Get the current user id, or `null` if no user is logged in. A reactive data source. //
* @locus Anywhere but publish functions //
*/ //
Meteor.userId = function () { // 221
return Accounts.userId(); // 222
}; //
//
/** //
* @summary Get the current user record, or `null` if no user is logged in. A reactive data source. //
* @locus Anywhere but publish functions //
*/ //
Meteor.user = function () { // 229
return Accounts.user(); // 230
}; //
//
// how long (in days) until a login token expires //
var DEFAULT_LOGIN_EXPIRATION_DAYS = 90; // 234
// Clients don't try to auto-login with a token that is going to expire within //
// .1 * DEFAULT_LOGIN_EXPIRATION_DAYS, capped at MIN_TOKEN_LIFETIME_CAP_SECS. //
// Tries to avoid abrupt disconnects from expiring tokens. //
var MIN_TOKEN_LIFETIME_CAP_SECS = 3600; // one hour // 238
// how often (in milliseconds) we check for expired tokens //
EXPIRE_TOKENS_INTERVAL_MS = 600 * 1000; // 10 minutes // 240
// how long we wait before logging out clients when Meteor.logoutOtherClients is //
// called //
CONNECTION_CLOSE_DELAY_MS = 10 * 1000; // 243
//
// loginServiceConfiguration and ConfigError are maintained for backwards compatibility //
Meteor.startup(function () { // 246
var ServiceConfiguration = Package['service-configuration'].ServiceConfiguration; // 247
Ap.loginServiceConfiguration = ServiceConfiguration.configurations; // 249
Ap.ConfigError = ServiceConfiguration.ConfigError; // 250
}); //
//
// Thrown when the user cancels the login process (eg, closes an oauth //
// popup, declines retina scan, etc) //
var lceName = 'Accounts.LoginCancelledError'; // 255
Ap.LoginCancelledError = Meteor.makeErrorType(lceName, function (description) { // 256
this.message = description; // 259
}); //
Ap.LoginCancelledError.prototype.name = lceName; // 262
//
// This is used to transmit specific subclass errors over the wire. We should //
// come up with a more generic way to do this (eg, with some sort of symbolic //
// error code rather than a number). //
Ap.LoginCancelledError.numericError = 0x8acdc2f; // 267
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/accounts-base/accounts_server.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
var crypto = Npm.require('crypto'); // 1
//
/** //
* @summary Constructor for the `Accounts` namespace on the server. //
* @locus Server //
* @class //
* @extends AccountsCommon //
* @instancename accountsServer //
* @param {Object} server A server object such as `Meteor.server`. //
*/ //
AccountsServer = (function (_AccountsCommon) { // 11
babelHelpers.inherits(AccountsServer, _AccountsCommon); //
//
// Note that this constructor is less likely to be instantiated multiple //
// times than the `AccountsClient` constructor, because a single server //
// can provide only one set of methods. //
//
function AccountsServer(server) { // 15
babelHelpers.classCallCheck(this, AccountsServer); //
//
_AccountsCommon.call(this); // 16
//
this._server = server || Meteor.server; // 18
// Set up the server's methods, as if by calling Meteor.methods. //
this._initServerMethods(); // 20
//
this._initAccountDataHooks(); // 22
//
// If autopublish is on, publish these user fields. Login service //
// packages (eg accounts-google) add to these by calling //
// addAutopublishFields. Notably, this isn't implemented with multiple //
// publishes since DDP only merges only across top-level fields, not //
// subfields (such as 'services.facebook.accessToken') //
this._autopublishFields = { // 29
loggedInUser: ['profile', 'username', 'emails'], // 30
otherUsers: ['profile', 'username'] // 31
}; //
this._initServerPublications(); // 33
//
// connectionId -> {connection, loginToken} //
this._accountData = {}; // 36
//
// connection id -> observe handle for the login token that this connection is //
// currently associated with, or a number. The number indicates that we are in //
// the process of setting up the observe (using a number instead of a single //
// sentinel allows multiple attempts to set up the observe to identify which //
// one was theirs). //
this._userObservesForConnections = {}; // 43
this._nextUserObserveNumber = 1; // for the number described above. // 44
//
// list of all registered handlers. //
this._loginHandlers = []; // 47
//
setupUsersCollection(this.users); // 49
setupDefaultLoginHandlers(this); // 50
setExpireTokensInterval(this); // 51
//
this._validateLoginHook = new Hook({ bindEnvironment: false }); // 53
this._validateNewUserHooks = [defaultValidateNewUserHook.bind(this)]; // 54
//
this._deleteSavedTokensForAllUsersOnStartup(); // 58
//
this._skipCaseInsensitiveChecksForTest = {}; // 60
} //
//
/// //
/// CURRENT USER //
/// //
//
// @override of "abstract" non-implementation in accounts_common.js //
//
AccountsServer.prototype.userId = (function () { // 11
function userId() { // 68
// This function only works if called inside a method. In theory, it //
// could also be called from publish statements, since they also //
// have a userId associated with them. However, given that publish //
// functions aren't reactive, using any of the infomation from //
// Meteor.user() in a publish function will always use the value //
// from when the function first runs. This is likely not what the //
// user expects. The way to make this work in a publish is to do //
// Meteor.find(this.userId).observe and recompute when the user //
// record changes. //
var currentInvocation = DDP._CurrentInvocation.get(); // 78
if (!currentInvocation) throw new Error("Meteor.userId can only be invoked in method calls. Use this.userId in publish functions.");
return currentInvocation.userId; // 81
} //
//
return userId; //
})(); //
//
/// //
/// LOGIN HOOKS //
/// //
//
/** //
* @summary Validate login attempts. //
* @locus Server //
* @param {Function} func Called whenever a login is attempted (either successful or unsuccessful). A login can be aborted by returning a falsy value or throwing an exception.
*/ //
//
AccountsServer.prototype.validateLoginAttempt = (function () { // 11
function validateLoginAttempt(func) { // 93
// Exceptions inside the hook callback are passed up to us. //
return this._validateLoginHook.register(func); // 95
} //
//
return validateLoginAttempt; //
})(); //
//
/** //
* @summary Set restrictions on new user creation. //
* @locus Server //
* @param {Function} func Called whenever a new user is created. Takes the new user object, and returns true to allow the creation or false to abort.
*/ //
//
AccountsServer.prototype.validateNewUser = (function () { // 11
function validateNewUser(func) { // 103
this._validateNewUserHooks.push(func); // 104
} //
//
return validateNewUser; //
})(); //
//
/// //
/// CREATE USER HOOKS //
/// //
//
/** //
* @summary Customize new user creation. //
* @locus Server //
* @param {Function} func Called whenever a new user is created. Return the new user object, or throw an `Error` to abort the creation.
*/ //
//
AccountsServer.prototype.onCreateUser = (function () { // 11
function onCreateUser(func) { // 116
if (this._onCreateUserHook) { // 117
throw new Error("Can only call onCreateUser once"); // 118
} //
//
this._onCreateUserHook = func; // 121
} //
//
return onCreateUser; //
})(); //
//
return AccountsServer; //
})(AccountsCommon); //
//
var Ap = AccountsServer.prototype; // 125
//
// Give each login hook callback a fresh cloned copy of the attempt //
// object, but don't clone the connection. //
// //
function cloneAttemptWithConnection(connection, attempt) { // 130
var clonedAttempt = EJSON.clone(attempt); // 131
clonedAttempt.connection = connection; // 132
return clonedAttempt; // 133
} //
//
Ap._validateLogin = function (connection, attempt) { // 136
this._validateLoginHook.each(function (callback) { // 137
var ret; // 138
try { // 139
ret = callback(cloneAttemptWithConnection(connection, attempt)); // 140
} catch (e) { //
attempt.allowed = false; // 143
// XXX this means the last thrown error overrides previous error //
// messages. Maybe this is surprising to users and we should make //
// overriding errors more explicit. (see //
// https://github.com/meteor/meteor/issues/1960) //
attempt.error = e; // 148
return true; // 149
} //
if (!ret) { // 151
attempt.allowed = false; // 152
// don't override a specific error provided by a previous //
// validator or the initial attempt (eg "incorrect password"). //
if (!attempt.error) attempt.error = new Meteor.Error(403, "Login forbidden"); // 155
} //
return true; // 158
}); //
}; //
//
Ap._successfulLogin = function (connection, attempt) { // 163
this._onLoginHook.each(function (callback) { // 164
callback(cloneAttemptWithConnection(connection, attempt)); // 165
return true; // 166
}); //
}; //
//
Ap._failedLogin = function (connection, attempt) { // 170
this._onLoginFailureHook.each(function (callback) { // 171
callback(cloneAttemptWithConnection(connection, attempt)); // 172
return true; // 173
}); //
}; //
//
/// //
/// LOGIN METHODS //
/// //
//
// Login methods return to the client an object containing these //
// fields when the user was logged in successfully: //
// //
// id: userId //
// token: * //
// tokenExpires: * //
// //
// tokenExpires is optional and intends to provide a hint to the //
// client as to when the token will expire. If not provided, the //
// client will call Accounts._tokenExpiration, passing it the date //
// that it received the token. //
// //
// The login method will throw an error back to the client if the user //
// failed to log in. //
// //
// //
// Login handlers and service specific login methods such as //
// `createUser` internally return a `result` object containing these //
// fields: //
// //
// type: //
// optional string; the service name, overrides the handler //
// default if present. //
// //
// error: //
// exception; if the user is not allowed to login, the reason why. //
// //
// userId: //
// string; the user id of the user attempting to login (if //
// known), required for an allowed login. //
// //
// options: //
// optional object merged into the result returned by the login //
// method; used by HAMK from SRP. //
// //
// stampedLoginToken: //
// optional object with `token` and `when` indicating the login //
// token is already present in the database, returned by the //
// "resume" login handler. //
// //
// For convenience, login methods can also throw an exception, which //
// is converted into an {error} result. However, if the id of the //
// user attempting the login is known, a {userId, error} result should //
// be returned instead since the user id is not captured when an //
// exception is thrown. //
// //
// This internal `result` object is automatically converted into the //
// public {id, token, tokenExpires} object returned to the client. //
//
// Try a login method, converting thrown exceptions into an {error} //
// result. The `type` argument is a default, inserted into the result //
// object if not explicitly returned. //
// //
var tryLoginMethod = function (type, fn) { // 236
var result; // 237
try { // 238
result = fn(); // 239
} catch (e) { //
result = { error: e }; // 242
} //
//
if (result && !result.type && type) result.type = type; // 245
//
return result; // 248
}; //
//
// Log in a user on a connection. //
// //
// We use the method invocation to set the user id on the connection, //
// not the connection object directly. setUserId is tied to methods to //
// enforce clear ordering of method application (using wait methods on //
// the client, and a no setUserId after unblock restriction on the //
// server) //
// //
// The `stampedLoginToken` parameter is optional. When present, it //
// indicates that the login token has already been inserted into the //
// database and doesn't need to be inserted again. (It's used by the //
// "resume" login handler). //
Ap._loginUser = function (methodInvocation, userId, stampedLoginToken) { // 264
var self = this; // 265
//
if (!stampedLoginToken) { // 267
stampedLoginToken = self._generateStampedLoginToken(); // 268
self._insertLoginToken(userId, stampedLoginToken); // 269
} //
//
// This order (and the avoidance of yields) is important to make //
// sure that when publish functions are rerun, they see a //
// consistent view of the world: the userId is set and matches //
// the login token on the connection (not that there is //
// currently a public API for reading the login token on a //
// connection). //
Meteor._noYieldsAllowed(function () { // 278
self._setLoginToken(userId, methodInvocation.connection, self._hashLoginToken(stampedLoginToken.token)); // 279
}); //
//
methodInvocation.setUserId(userId); // 286
//
return { // 288
id: userId, // 289
token: stampedLoginToken.token, // 290
tokenExpires: self._tokenExpiration(stampedLoginToken.when) // 291
}; //
}; //
//
// After a login method has completed, call the login hooks. Note //
// that `attemptLogin` is called for *all* login attempts, even ones //
// which aren't successful (such as an invalid password, etc). //
// //
// If the login is allowed and isn't aborted by a validate login hook //
// callback, log in the user. //
// //
Ap._attemptLogin = function (methodInvocation, methodName, methodArgs, result) { // 303
if (!result) throw new Error("result is required"); // 309
//
// XXX A programming error in a login handler can lead to this occuring, and //
// then we don't call onLogin or onLoginFailure callbacks. Should //
// tryLoginMethod catch this case and turn it into an error? //
if (!result.userId && !result.error) throw new Error("A login method must specify a userId or an error"); // 315
//
var user; // 318
if (result.userId) user = this.users.findOne(result.userId); // 319
//
var attempt = { // 322
type: result.type || "unknown", // 323
allowed: !!(result.userId && !result.error), // 324
methodName: methodName, // 325
methodArguments: _.toArray(methodArgs) // 326
}; //
if (result.error) attempt.error = result.error; // 328
if (user) attempt.user = user; // 330
//
// _validateLogin may mutate `attempt` by adding an error and changing allowed //
// to false, but that's the only change it can make (and the user's callbacks //
// only get a clone of `attempt`). //
this._validateLogin(methodInvocation.connection, attempt); // 336
//
if (attempt.allowed) { // 338
var ret = _.extend(this._loginUser(methodInvocation, result.userId, result.stampedLoginToken), result.options || {});
this._successfulLogin(methodInvocation.connection, attempt); // 347
return ret; // 348
} else { //
this._failedLogin(methodInvocation.connection, attempt); // 351
throw attempt.error; // 352
} //
}; //
//
// All service specific login methods should go through this function. //
// Ensure that thrown exceptions are caught and that login hook //
// callbacks are still called. //
// //
Ap._loginMethod = function (methodInvocation, methodName, methodArgs, type, fn) { // 361
return this._attemptLogin(methodInvocation, methodName, methodArgs, tryLoginMethod(type, fn)); // 368
}; //
//
// Report a login attempt failed outside the context of a normal login //
// method. This is for use in the case where there is a multi-step login //
// procedure (eg SRP based password login). If a method early in the //
// chain fails, it should call this function to report a failure. There //
// is no corresponding method for a successful login; methods that can //
// succeed at logging a user in should always be actual login methods //
// (using either Accounts._loginMethod or Accounts.registerLoginHandler). //
Ap._reportLoginFailure = function (methodInvocation, methodName, methodArgs, result) { // 384
var attempt = { // 390
type: result.type || "unknown", // 391
allowed: false, // 392
error: result.error, // 393
methodName: methodName, // 394
methodArguments: _.toArray(methodArgs) // 395
}; //
//
if (result.userId) { // 398
attempt.user = this.users.findOne(result.userId); // 399
} //
//
this._validateLogin(methodInvocation.connection, attempt); // 402
this._failedLogin(methodInvocation.connection, attempt); // 403
//
// _validateLogin may mutate attempt to set a new error message. Return //
// the modified version. //
return attempt; // 407
}; //
//
/// //
/// LOGIN HANDLERS //
/// //
//
// The main entry point for auth packages to hook in to login. //
// //
// A login handler is a login method which can return `undefined` to //
// indicate that the login request is not handled by this handler. //
// //
// @param name {String} Optional. The service name, used by default //
// if a specific service name isn't returned in the result. //
// //
// @param handler {Function} A function that receives an options object //
// (as passed as an argument to the `login` method) and returns one of: //
// - `undefined`, meaning don't handle; //
// - a login method result object //
//
Ap.registerLoginHandler = function (name, handler) { // 428
if (!handler) { // 429
handler = name; // 430
name = null; // 431
} //
//
this._loginHandlers.push({ // 434
name: name, // 435
handler: handler // 436
}); //
}; //
//
// Checks a user's credentials against all the registered login //
// handlers, and returns a login token if the credentials are valid. It //
// is like the login method, except that it doesn't set the logged-in //
// user on the connection. Throws a Meteor.Error if logging in fails, //
// including the case where none of the login handlers handled the login //
// request. Otherwise, returns {id: userId, token: *, tokenExpires: *}. //
// //
// For example, if you want to login with a plaintext password, `options` could be //
// { user: { username: <username> }, password: <password> }, or //
// { user: { email: <email> }, password: <password> }. //
//
// Try all of the registered login handlers until one of them doesn't //
// return `undefined`, meaning it handled this call to `login`. Return //
// that return value. //
Ap._runLoginHandlers = function (methodInvocation, options) { // 455
for (var i = 0; i < this._loginHandlers.length; ++i) { // 456
var handler = this._loginHandlers[i]; // 457
//
var result = tryLoginMethod(handler.name, function () { // 459
return handler.handler.call(methodInvocation, options); // 462
}); //
//
if (result) { // 466
return result; // 467
} //
//
if (result !== undefined) { // 470
throw new Meteor.Error(400, "A login handler should return a result or undefined"); // 471
} //
} //
//
return { // 475
type: null, // 476
error: new Meteor.Error(400, "Unrecognized options for login request") // 477
}; //
}; //
//
// Deletes the given loginToken from the database. //
// //
// For new-style hashed token, this will cause all connections //
// associated with the token to be closed. //
// //
// Any connections associated with old-style unhashed tokens will be //
// in the process of becoming associated with hashed tokens and then //
// they'll get closed. //
Ap.destroyToken = function (userId, loginToken) { // 489
this.users.update(userId, { // 490
$pull: { // 491
"services.resume.loginTokens": { // 492
$or: [{ hashedToken: loginToken }, { token: loginToken }] // 493
} //
} //
}); //
}; //
//
Ap._initServerMethods = function () { // 502
// The methods created in this function need to be created here so that //
// this variable is available in their scope. //
var accounts = this; // 505
//
// This object will be populated with methods and then passed to //
// accounts._server.methods further below. //
var methods = {}; // 509
//
// @returns {Object|null} //
// If successful, returns {token: reconnectToken, id: userId} //
// If unsuccessful (for example, if the user closed the oauth login popup), //
// throws an error describing the reason //
methods.login = function (options) { // 515
var self = this; // 516
//
// Login handlers should really also check whatever field they look at in //
// options, but we don't enforce it. //
check(options, Object); // 520
//
var result = accounts._runLoginHandlers(self, options); // 522
//
return accounts._attemptLogin(self, "login", arguments, result); // 524
}; //
//
methods.logout = function () { // 527
var token = accounts._getLoginToken(this.connection.id); // 528
accounts._setLoginToken(this.userId, this.connection, null); // 529
if (token && this.userId) accounts.destroyToken(this.userId, token); // 530
this.setUserId(null); // 532
}; //
//
// Delete all the current user's tokens and close all open connections logged //
// in as this user. Returns a fresh new login token that this client can //
// use. Tests set Accounts._noConnectionCloseDelayForTest to delete tokens //
// immediately instead of using a delay. //
// //
// XXX COMPAT WITH 0.7.2 //
// This single `logoutOtherClients` method has been replaced with two //
// methods, one that you call to get a new token, and another that you //
// call to remove all tokens except your own. The new design allows //
// clients to know when other clients have actually been logged //
// out. (The `logoutOtherClients` method guarantees the caller that //
// the other clients will be logged out at some point, but makes no //
// guarantees about when.) This method is left in for backwards //
// compatibility, especially since application code might be calling //
// this method directly. //
// //
// @returns {Object} Object with token and tokenExpires keys. //
methods.logoutOtherClients = function () { // 552
var self = this; // 553
var user = accounts.users.findOne(self.userId, { // 554
fields: { // 555
"services.resume.loginTokens": true // 556
} //
}); //
if (user) { // 559
// Save the current tokens in the database to be deleted in //
// CONNECTION_CLOSE_DELAY_MS ms. This gives other connections in the //
// caller's browser time to find the fresh token in localStorage. We save //
// the tokens in the database in case we crash before actually deleting //
// them. //
var tokens = user.services.resume.loginTokens; // 565
var newToken = accounts._generateStampedLoginToken(); // 566
var userId = self.userId; // 567
accounts.users.update(userId, { // 568
$set: { // 569
"services.resume.loginTokensToDelete": tokens, // 570
"services.resume.haveLoginTokensToDelete": true // 571
}, //
$push: { "services.resume.loginTokens": accounts._hashStampedToken(newToken) } // 573
}); //
Meteor.setTimeout(function () { // 575
// The observe on Meteor.users will take care of closing the connections //
// associated with `tokens`. //
accounts._deleteSavedTokensForUser(userId, tokens); // 578
}, accounts._noConnectionCloseDelayForTest ? 0 : CONNECTION_CLOSE_DELAY_MS); //
// We do not set the login token on this connection, but instead the //
// observe closes the connection and the client will reconnect with the //
// new token. //
return { // 584
token: newToken.token, // 585
tokenExpires: accounts._tokenExpiration(newToken.when) // 586
}; //
} else { //
throw new Meteor.Error("You are not logged in."); // 589
} //
}; //
//
// Generates a new login token with the same expiration as the //
// connection's current token and saves it to the database. Associates //
// the connection with this new token and returns it. Throws an error //
// if called on a connection that isn't logged in. //
// //
// @returns Object //
// If successful, returns { token: <new token>, id: <user id>, //
// tokenExpires: <expiration date> }. //
methods.getNewToken = function () { // 601
var self = this; // 602
var user = accounts.users.findOne(self.userId, { // 603
fields: { "services.resume.loginTokens": 1 } // 604
}); //
if (!self.userId || !user) { // 606
throw new Meteor.Error("You are not logged in."); // 607
} //
// Be careful not to generate a new token that has a later //
// expiration than the curren token. Otherwise, a bad guy with a //
// stolen token could use this method to stop his stolen token from //
// ever expiring. //
var currentHashedToken = accounts._getLoginToken(self.connection.id); // 613
var currentStampedToken = _.find(user.services.resume.loginTokens, function (stampedToken) { // 614
return stampedToken.hashedToken === currentHashedToken; // 617
}); //
if (!currentStampedToken) { // 620
// safety belt: this should never happen //
throw new Meteor.Error("Invalid login token"); // 621
} //
var newStampedToken = accounts._generateStampedLoginToken(); // 623
newStampedToken.when = currentStampedToken.when; // 624
accounts._insertLoginToken(self.userId, newStampedToken); // 625
return accounts._loginUser(self, self.userId, newStampedToken); // 626
}; //
//
// Removes all tokens except the token associated with the current //
// connection. Throws an error if the connection is not logged //
// in. Returns nothing on success. //
methods.removeOtherTokens = function () { // 632
var self = this; // 633
if (!self.userId) { // 634
throw new Meteor.Error("You are not logged in."); // 635
} //
var currentToken = accounts._getLoginToken(self.connection.id); // 637
accounts.users.update(self.userId, { // 638
$pull: { // 639
"services.resume.loginTokens": { hashedToken: { $ne: currentToken } } // 640
} //
}); //
}; //
//
// Allow a one-time configuration for a login service. Modifications //
// to this collection are also allowed in insecure mode. //
methods.configureLoginService = function (options) { // 647
check(options, Match.ObjectIncluding({ service: String })); // 648
// Don't let random users configure a service we haven't added yet (so //
// that when we do later add it, it's set up with their configuration //
// instead of ours). //
// XXX if service configuration is oauth-specific then this code should //
// be in accounts-oauth; if it's not then the registry should be //
// in this package //
if (!(accounts.oauth && _.contains(accounts.oauth.serviceNames(), options.service))) { // 655
throw new Meteor.Error(403, "Service unknown"); // 657
} //
//
var ServiceConfiguration = Package['service-configuration'].ServiceConfiguration; // 660
if (ServiceConfiguration.configurations.findOne({ service: options.service })) throw new Meteor.Error(403, "Service " + options.service + " already configured");
//
if (_.has(options, "secret") && usingOAuthEncryption()) options.secret = OAuthEncryption.seal(options.secret);
//
ServiceConfiguration.configurations.insert(options); // 668
}; //
//
accounts._server.methods(methods); // 671
}; //
//
Ap._initAccountDataHooks = function () { // 674
var accounts = this; // 675
//
accounts._server.onConnection(function (connection) { // 677
accounts._accountData[connection.id] = { // 678
connection: connection // 679
}; //
//
connection.onClose(function () { // 682
accounts._removeTokenFromConnection(connection.id); // 683
delete accounts._accountData[connection.id]; // 684
}); //
}); //
}; //
//
Ap._initServerPublications = function () { // 689
var accounts = this; // 690
//
// Publish all login service configuration fields other than secret. //
accounts._server.publish("meteor.loginServiceConfiguration", function () { // 693
var ServiceConfiguration = Package['service-configuration'].ServiceConfiguration; // 694
return ServiceConfiguration.configurations.find({}, { fields: { secret: 0 } }); // 696
}, { is_auto: true }); // not techincally autopublish, but stops the warning. //
//
// Publish the current user's record to the client. //
accounts._server.publish(null, function () { // 700
if (this.userId) { // 701
return accounts.users.find({ // 702
_id: this.userId // 703
}, { //
fields: { // 705
profile: 1, // 706
username: 1, // 707
emails: 1 // 708
} //
}); //
} else { //
return null; // 712
} //
}, /*suppress autopublish warning*/{ is_auto: true }); //
//
// Use Meteor.startup to give other packages a chance to call //
// addAutopublishFields. //
Package.autopublish && Meteor.startup(function () { // 718
// ['profile', 'username'] -> {profile: 1, username: 1} //
var toFieldSelector = function (fields) { // 720
return _.object(_.map(fields, function (field) { // 721
return [field, 1]; // 722
})); //
}; //
//
accounts._server.publish(null, function () { // 726
if (this.userId) { // 727
return accounts.users.find({ // 728
_id: this.userId // 729
}, { //
fields: toFieldSelector(accounts._autopublishFields.loggedInUser) // 731
}); //
} else { //
return null; // 734
} //
}, /*suppress autopublish warning*/{ is_auto: true }); //
//
// XXX this publish is neither dedup-able nor is it optimized by our special //
// treatment of queries on a specific _id. Therefore this will have O(n^2) //
// run-time performance every time a user document is changed (eg someone //
// logging in). If this is a problem, we can instead write a manual publish //
// function which filters out fields based on 'this.userId'. //
accounts._server.publish(null, function () { // 743
var selector = this.userId ? { // 744
_id: { $ne: this.userId } // 745
} : {}; //
//
return accounts.users.find(selector, { // 748
fields: toFieldSelector(accounts._autopublishFields.otherUsers) // 749
}); //
}, /*suppress autopublish warning*/{ is_auto: true }); //
}); //
}; //
//
// Add to the list of fields or subfields to be automatically //
// published if autopublish is on. Must be called from top-level //
// code (ie, before Meteor.startup hooks run). //
// //
// @param opts {Object} with: //
// - forLoggedInUser {Array} Array of fields published to the logged-in user //
// - forOtherUsers {Array} Array of fields published to users that aren't logged in //
Ap.addAutopublishFields = function (opts) { // 762
this._autopublishFields.loggedInUser.push.apply(this._autopublishFields.loggedInUser, opts.forLoggedInUser); // 763
this._autopublishFields.otherUsers.push.apply(this._autopublishFields.otherUsers, opts.forOtherUsers); // 765
}; //
//
/// //
/// ACCOUNT DATA //
/// //
//
// HACK: This is used by 'meteor-accounts' to get the loginToken for a //
// connection. Maybe there should be a public way to do that. //
Ap._getAccountData = function (connectionId, field) { // 775
var data = this._accountData[connectionId]; // 776
return data && data[field]; // 777
}; //
//
Ap._setAccountData = function (connectionId, field, value) { // 780
var data = this._accountData[connectionId]; // 781
//
// safety belt. shouldn't happen. accountData is set in onConnection, //
// we don't have a connectionId until it is set. //
if (!data) return; // 785
//
if (value === undefined) delete data[field];else data[field] = value; // 788
}; //
//
/// //
/// RECONNECT TOKENS //
/// //
/// support reconnecting using a meteor login token //
//
Ap._hashLoginToken = function (loginToken) { // 800
var hash = crypto.createHash('sha256'); // 801
hash.update(loginToken); // 802
return hash.digest('base64'); // 803
}; //
//
// {token, when} => {hashedToken, when} //
Ap._hashStampedToken = function (stampedToken) { // 808
return _.extend(_.omit(stampedToken, 'token'), { // 809
hashedToken: this._hashLoginToken(stampedToken.token) // 810
}); //
}; //
//
// Using $addToSet avoids getting an index error if another client //
// logging in simultaneously has already inserted the new hashed //
// token. //
Ap._insertHashedLoginToken = function (userId, hashedToken, query) { // 818
query = query ? _.clone(query) : {}; // 819
query._id = userId; // 820
this.users.update(query, { // 821
$addToSet: { // 822
"services.resume.loginTokens": hashedToken // 823
} //
}); //
}; //
//
// Exported for tests. //
Ap._insertLoginToken = function (userId, stampedToken, query) { // 830
this._insertHashedLoginToken(userId, this._hashStampedToken(stampedToken), query); // 831
}; //
//
Ap._clearAllLoginTokens = function (userId) { // 839
this.users.update(userId, { // 840
$set: { // 841
'services.resume.loginTokens': [] // 842
} //
}); //
}; //
//
// test hook //
Ap._getUserObserve = function (connectionId) { // 848
return this._userObservesForConnections[connectionId]; // 849
}; //
//
// Clean up this connection's association with the token: that is, stop //
// the observe that we started when we associated the connection with //
// this token. //
Ap._removeTokenFromConnection = function (connectionId) { // 855
if (_.has(this._userObservesForConnections, connectionId)) { // 856
var observe = this._userObservesForConnections[connectionId]; // 857
if (typeof observe === 'number') { // 858
// We're in the process of setting up an observe for this connection. We //
// can't clean up that observe yet, but if we delete the placeholder for //
// this connection, then the observe will get cleaned up as soon as it has //
// been set up. //
delete this._userObservesForConnections[connectionId]; // 863
} else { //
delete this._userObservesForConnections[connectionId]; // 865
observe.stop(); // 866
} //
} //
}; //
//
Ap._getLoginToken = function (connectionId) { // 871
return this._getAccountData(connectionId, 'loginToken'); // 872
}; //
//
// newToken is a hashed token. //
Ap._setLoginToken = function (userId, connection, newToken) { // 876
var self = this; // 877
//
self._removeTokenFromConnection(connection.id); // 879
self._setAccountData(connection.id, 'loginToken', newToken); // 880
//
if (newToken) { // 882
// Set up an observe for this token. If the token goes away, we need //
// to close the connection. We defer the observe because there's //
// no need for it to be on the critical path for login; we just need //
// to ensure that the connection will get closed at some point if //
// the token gets deleted. //
// //
// Initially, we set the observe for this connection to a number; this //
// signifies to other code (which might run while we yield) that we are in //
// the process of setting up an observe for this connection. Once the //
// observe is ready to go, we replace the number with the real observe //
// handle (unless the placeholder has been deleted or replaced by a //
// different placehold number, signifying that the connection was closed //
// already -- in this case we just clean up the observe that we started). //
var myObserveNumber = ++self._nextUserObserveNumber; // 896
self._userObservesForConnections[connection.id] = myObserveNumber; // 897
Meteor.defer(function () { // 898
// If something else happened on this connection in the meantime (it got //
// closed, or another call to _setLoginToken happened), just do //
// nothing. We don't need to start an observe for an old connection or old //
// token. //
if (self._userObservesForConnections[connection.id] !== myObserveNumber) { // 903
return; // 904
} //
//
var foundMatchingUser; // 907
// Because we upgrade unhashed login tokens to hashed tokens at //
// login time, sessions will only be logged in with a hashed //
// token. Thus we only need to observe hashed tokens here. //
var observe = self.users.find({ // 911
_id: userId, // 912
'services.resume.loginTokens.hashedToken': newToken // 913
}, { fields: { _id: 1 } }).observeChanges({ //
added: function () { // 915
foundMatchingUser = true; // 916
}, //
removed: function () { // 918
connection.close(); // 919
// The onClose callback for the connection takes care of //
// cleaning up the observe handle and any other state we have //
// lying around. //
} //
}); //
//
// If the user ran another login or logout command we were waiting for the //
// defer or added to fire (ie, another call to _setLoginToken occurred), //
// then we let the later one win (start an observe, etc) and just stop our //
// observe now. //
// //
// Similarly, if the connection was already closed, then the onClose //
// callback would have called _removeTokenFromConnection and there won't //
// be an entry in _userObservesForConnections. We can stop the observe. //
if (self._userObservesForConnections[connection.id] !== myObserveNumber) { // 934
observe.stop(); // 935
return; // 936
} //
//
self._userObservesForConnections[connection.id] = observe; // 939
//
if (!foundMatchingUser) { // 941
// We've set up an observe on the user associated with `newToken`, //
// so if the new token is removed from the database, we'll close //
// the connection. But the token might have already been deleted //
// before we set up the observe, which wouldn't have closed the //
// connection because the observe wasn't running yet. //
connection.close(); // 947
} //
}); //
} //
}; //
//
function setupDefaultLoginHandlers(accounts) { // 953
accounts.registerLoginHandler("resume", function (options) { // 954
return defaultResumeLoginHandler.call(this, accounts, options); // 955
}); //
} //
//
// Login handler for resume tokens. //
function defaultResumeLoginHandler(accounts, options) { // 960
if (!options.resume) return undefined; // 961
//
check(options.resume, String); // 964
//
var hashedToken = accounts._hashLoginToken(options.resume); // 966
//
// First look for just the new-style hashed login token, to avoid //
// sending the unhashed token to the database in a query if we don't //
// need to. //
var user = accounts.users.findOne({ "services.resume.loginTokens.hashedToken": hashedToken }); // 971
//
if (!user) { // 974
// If we didn't find the hashed login token, try also looking for //
// the old-style unhashed token. But we need to look for either //
// the old-style token OR the new-style token, because another //
// client connection logging in simultaneously might have already //
// converted the token. //
user = accounts.users.findOne({ // 980
$or: [{ "services.resume.loginTokens.hashedToken": hashedToken }, { "services.resume.loginTokens.token": options.resume }]
}); //
} //
//
if (!user) return { // 988
error: new Meteor.Error(403, "You've been logged out by the server. Please log in again.") // 990
}; //
//
// Find the token, which will either be an object with fields //
// {hashedToken, when} for a hashed token or {token, when} for an //
// unhashed token. //
var oldUnhashedStyleToken; // 996
var token = _.find(user.services.resume.loginTokens, function (token) { // 997
return token.hashedToken === hashedToken; // 998
}); //
if (token) { // 1000
oldUnhashedStyleToken = false; // 1001
} else { //
token = _.find(user.services.resume.loginTokens, function (token) { // 1003
return token.token === options.resume; // 1004
}); //
oldUnhashedStyleToken = true; // 1006
} //
//
var tokenExpires = accounts._tokenExpiration(token.when); // 1009
if (new Date() >= tokenExpires) return { // 1010
userId: user._id, // 1012
error: new Meteor.Error(403, "Your session has expired. Please log in again.") // 1013
}; //
//
// Update to a hashed token when an unhashed token is encountered. //
if (oldUnhashedStyleToken) { // 1017
// Only add the new hashed token if the old unhashed token still //
// exists (this avoids resurrecting the token if it was deleted //
// after we read it). Using $addToSet avoids getting an index //
// error if another client logging in simultaneously has already //
// inserted the new hashed token. //
accounts.users.update({ // 1023
_id: user._id, // 1025
"services.resume.loginTokens.token": options.resume // 1026
}, { $addToSet: { //
"services.resume.loginTokens": { // 1029
"hashedToken": hashedToken, // 1030
"when": token.when // 1031
} //
} }); //
//
// Remove the old token *after* adding the new, since otherwise //
// another client trying to login between our removing the old and //
// adding the new wouldn't find a token to login with. //
accounts.users.update(user._id, { // 1039
$pull: { // 1040
"services.resume.loginTokens": { "token": options.resume } // 1041
} //
}); //
} //
//
return { // 1046
userId: user._id, // 1047
stampedLoginToken: { // 1048
token: options.resume, // 1049
when: token.when // 1050
} //
}; //
} //
//
// (Also used by Meteor Accounts server and tests). //
// //
Ap._generateStampedLoginToken = function () { // 1057
return { // 1058
token: Random.secret(), // 1059
when: new Date() // 1060
}; //
}; //
//
/// //
/// TOKEN EXPIRATION //
/// //
//
// Deletes expired tokens from the database and closes all open connections //
// associated with these tokens. //
// //
// Exported for tests. Also, the arguments are only used by //
// tests. oldestValidDate is simulate expiring tokens without waiting //
// for them to actually expire. userId is used by tests to only expire //
// tokens for the test user. //
Ap._expireTokens = function (oldestValidDate, userId) { // 1075
var tokenLifetimeMs = this._getTokenLifetimeMs(); // 1076
//
// when calling from a test with extra arguments, you must specify both! //
if (oldestValidDate && !userId || !oldestValidDate && userId) { // 1079
throw new Error("Bad test. Must specify both oldestValidDate and userId."); // 1080
} //
//
oldestValidDate = oldestValidDate || new Date(new Date() - tokenLifetimeMs); // 1083
var userFilter = userId ? { _id: userId } : {}; // 1085
//
// Backwards compatible with older versions of meteor that stored login token //
// timestamps as numbers. //
this.users.update(_.extend(userFilter, { // 1090
$or: [{ "services.resume.loginTokens.when": { $lt: oldestValidDate } }, { "services.resume.loginTokens.when": { $lt: +oldestValidDate } }]
}), { //
$pull: { // 1096
"services.resume.loginTokens": { // 1097
$or: [{ when: { $lt: oldestValidDate } }, { when: { $lt: +oldestValidDate } }] // 1098
} //
} //
}, { multi: true }); //
// The observe on Meteor.users will take care of closing connections for //
// expired tokens. //
}; //
//
// @override from accounts_common.js //
Ap.config = function (options) { // 1110
// Call the overridden implementation of the method. //
var superResult = AccountsCommon.prototype.config.apply(this, arguments); // 1112
//
// If the user set loginExpirationInDays to null, then we need to clear the //
// timer that periodically expires tokens. //
if (_.has(this._options, "loginExpirationInDays") && this._options.loginExpirationInDays === null && this.expireTokenInterval) {
Meteor.clearInterval(this.expireTokenInterval); // 1119
this.expireTokenInterval = null; // 1120
} //
//
return superResult; // 1123
}; //
//
function setExpireTokensInterval(accounts) { // 1126
accounts.expireTokenInterval = Meteor.setInterval(function () { // 1127
accounts._expireTokens(); // 1128
}, EXPIRE_TOKENS_INTERVAL_MS); //
} //
//
/// //
/// OAuth Encryption Support //
/// //
//
var OAuthEncryption = Package["oauth-encryption"] && Package["oauth-encryption"].OAuthEncryption; // 1137
//
function usingOAuthEncryption() { // 1141
return OAuthEncryption && OAuthEncryption.keyIsLoaded(); // 1142
} //
//
// OAuth service data is temporarily stored in the pending credentials //
// collection during the oauth authentication process. Sensitive data //
// such as access tokens are encrypted without the user id because //
// we don't know the user id yet. We re-encrypt these fields with the //
// user id included when storing the service data permanently in //
// the users collection. //
// //
function pinEncryptedFieldsToUser(serviceData, userId) { // 1153
_.each(_.keys(serviceData), function (key) { // 1154
var value = serviceData[key]; // 1155
if (OAuthEncryption && OAuthEncryption.isSealed(value)) value = OAuthEncryption.seal(OAuthEncryption.open(value), userId);
serviceData[key] = value; // 1158
}); //
} //
//
// Encrypt unencrypted login service secrets when oauth-encryption is //
// added. //
// //
// XXX For the oauthSecretKey to be available here at startup, the //
// developer must call Accounts.config({oauthSecretKey: ...}) at load //
// time, instead of in a Meteor.startup block, because the startup //
// block in the app code will run after this accounts-base startup //
// block. Perhaps we need a post-startup callback? //
//
Meteor.startup(function () { // 1172
if (!usingOAuthEncryption()) { // 1173
return; // 1174
} //
//
var ServiceConfiguration = Package['service-configuration'].ServiceConfiguration; // 1177
//
ServiceConfiguration.configurations.find({ // 1180
$and: [{ // 1181
secret: { $exists: true } // 1182
}, { //
"secret.algorithm": { $exists: false } // 1184
}] //
}).forEach(function (config) { //
ServiceConfiguration.configurations.update(config._id, { // 1187
$set: { // 1188
secret: OAuthEncryption.seal(config.secret) // 1189
} //
}); //
}); //
}); //
//
// XXX see comment on Accounts.createUser in passwords_server about adding a //
// second "server options" argument. //
function defaultCreateUserHook(options, user) { // 1197
if (options.profile) user.profile = options.profile; // 1198
return user; // 1200
} //
//
// Called by accounts-password //
Ap.insertUserDoc = function (options, user) { // 1204
// - clone user document, to protect from modification //
// - add createdAt timestamp //
// - prepare an _id, so that you can modify other collections (eg //
// create a first task for every new user) //
// //
// XXX If the onCreateUser or validateNewUser hooks fail, we might //
// end up having modified some other collection //
// inappropriately. The solution is probably to have onCreateUser //
// accept two callbacks - one that gets called before inserting //
// the user document (in which you can modify its contents), and //
// one that gets called after (in which you should change other //
// collections) //
user = _.extend({ // 1217
createdAt: new Date(), // 1218
_id: Random.id() // 1219
}, user); //
//
if (user.services) { // 1222
_.each(user.services, function (serviceData) { // 1223
pinEncryptedFieldsToUser(serviceData, user._id); // 1224
}); //
} //
//
var fullUser; // 1228
if (this._onCreateUserHook) { // 1229
fullUser = this._onCreateUserHook(options, user); // 1230
//
// This is *not* part of the API. We need this because we can't isolate //
// the global server environment between tests, meaning we can't test //
// both having a create user hook set and not having one set. //
if (fullUser === 'TEST DEFAULT HOOK') fullUser = defaultCreateUserHook(options, user); // 1235
} else { //
fullUser = defaultCreateUserHook(options, user); // 1238
} //
//
_.each(this._validateNewUserHooks, function (hook) { // 1241
if (!hook(fullUser)) throw new Meteor.Error(403, "User validation failed"); // 1242
}); //
//
var userId; // 1246
try { // 1247
userId = this.users.insert(fullUser); // 1248
} catch (e) { //
// XXX string parsing sucks, maybe //
// https://jira.mongodb.org/browse/SERVER-3069 will get fixed one day //
if (e.name !== 'MongoError') throw e; // 1252
if (e.code !== 11000) throw e; // 1253
if (e.err.indexOf('emails.address') !== -1) throw new Meteor.Error(403, "Email already exists."); // 1254
if (e.err.indexOf('username') !== -1) throw new Meteor.Error(403, "Username already exists."); // 1256
// XXX better error reporting for services.facebook.id duplicate, etc //
throw e; // 1259
} //
return userId; // 1261
}; //
//
// Helper function: returns false if email does not match company domain from //
// the configuration. //
Ap._testEmailDomain = function (email) { // 1266
var domain = this._options.restrictCreationByEmailDomain; // 1267
return !domain || _.isFunction(domain) && domain(email) || _.isString(domain) && new RegExp('@' + Meteor._escapeRegExp(domain) + '$', 'i').test(email);
}; //
//
// Validate new user's email or Google/Facebook/GitHub account's email //
function defaultValidateNewUserHook(user) { // 1275
var self = this; // 1276
var domain = self._options.restrictCreationByEmailDomain; // 1277
if (!domain) return true; // 1278
//
var emailIsGood = false; // 1281
if (!_.isEmpty(user.emails)) { // 1282
emailIsGood = _.any(user.emails, function (email) { // 1283
return self._testEmailDomain(email.address); // 1284
}); //
} else if (!_.isEmpty(user.services)) { //
// Find any email of any service and check it //
emailIsGood = _.any(user.services, function (service) { // 1288
return service.email && self._testEmailDomain(service.email); // 1289
}); //
} //
//
if (emailIsGood) return true; // 1293
//
if (_.isString(domain)) throw new Meteor.Error(403, "@" + domain + " email required");else throw new Meteor.Error(403, "Email doesn't match the criteria.");
} //
//
/// //
/// MANAGING USER OBJECTS //
/// //
//
// Updates or creates a user after we authenticate with a 3rd party. //
// //
// @param serviceName {String} Service name (eg, twitter). //
// @param serviceData {Object} Data to store in the user's record //
// under services[serviceName]. Must include an "id" field //
// which is a unique identifier for the user in the service. //
// @param options {Object, optional} Other options to pass to insertUserDoc //
// (eg, profile) //
// @returns {Object} Object with token and id keys, like the result //
// of the "login" method. //
// //
Ap.updateOrCreateUserFromExternalService = function (serviceName, serviceData, options) { // 1317
options = _.clone(options || {}); // 1322
//
if (serviceName === "password" || serviceName === "resume") throw new Error("Can't use updateOrCreateUserFromExternalService with internal service " + serviceName);
if (!_.has(serviceData, 'id')) throw new Error("Service data for service " + serviceName + " must include id"); // 1328
//
// Look for a user with the appropriate service user id. //
var selector = {}; // 1333
var serviceIdKey = "services." + serviceName + ".id"; // 1334
//
// XXX Temporary special case for Twitter. (Issue #629) //
// The serviceData.id will be a string representation of an integer. //
// We want it to match either a stored string or int representation. //
// This is to cater to earlier versions of Meteor storing twitter //
// user IDs in number form, and recent versions storing them as strings. //
// This can be removed once migration technology is in place, and twitter //
// users stored with integer IDs have been migrated to string IDs. //
if (serviceName === "twitter" && !isNaN(serviceData.id)) { // 1343
selector["$or"] = [{}, {}]; // 1344
selector["$or"][0][serviceIdKey] = serviceData.id; // 1345
selector["$or"][1][serviceIdKey] = parseInt(serviceData.id, 10); // 1346
} else { //
selector[serviceIdKey] = serviceData.id; // 1348
} //
//
var user = this.users.findOne(selector); // 1351
//
if (user) { // 1353
pinEncryptedFieldsToUser(serviceData, user._id); // 1354
//
// We *don't* process options (eg, profile) for update, but we do replace //
// the serviceData (eg, so that we keep an unexpired access token and //
// don't cache old email addresses in serviceData.email). //
// XXX provide an onUpdateUser hook which would let apps update //
// the profile too //
var setAttrs = {}; // 1361
_.each(serviceData, function (value, key) { // 1362
setAttrs["services." + serviceName + "." + key] = value; // 1363
}); //
//
// XXX Maybe we should re-use the selector above and notice if the update //
// touches nothing? //
this.users.update(user._id, { // 1368
$set: setAttrs // 1369
}); //
//
return { // 1372
type: serviceName, // 1373
userId: user._id // 1374
}; //
} else { //
// Create a new user with the service data. Pass other options through to //
// insertUserDoc. //
user = { services: {} }; // 1380
user.services[serviceName] = serviceData; // 1381
return { // 1382
type: serviceName, // 1383
userId: this.insertUserDoc(options, user) // 1384
}; //
} //
}; //
//
function setupUsersCollection(users) { // 1389
/// //
/// RESTRICTING WRITES TO USER OBJECTS //
/// //
users.allow({ // 1393
// clients can modify the profile field of their own document, and //
// nothing else. //
update: function (userId, user, fields, modifier) { // 1396
// make sure it is our record //
if (user._id !== userId) return false; // 1398
//
// user can only modify the 'profile' field. sets to multiple //
// sub-keys (eg profile.foo and profile.bar) are merged into entry //
// in the fields list. //
if (fields.length !== 1 || fields[0] !== 'profile') return false; // 1404
//
return true; // 1407
}, //
fetch: ['_id'] // we only look at _id. // 1409
}); //
//
/// DEFAULT INDEXES ON USERS //
users._ensureIndex('username', { unique: 1, sparse: 1 }); // 1413
users._ensureIndex('emails.address', { unique: 1, sparse: 1 }); // 1414
users._ensureIndex('services.resume.loginTokens.hashedToken', { unique: 1, sparse: 1 }); // 1415
users._ensureIndex('services.resume.loginTokens.token', { unique: 1, sparse: 1 }); // 1417
// For taking care of logoutOtherClients calls that crashed before the //
// tokens were deleted. //
users._ensureIndex('services.resume.haveLoginTokensToDelete', { sparse: 1 }); // 1421
// For expiring login tokens //
users._ensureIndex("services.resume.loginTokens.when", { sparse: 1 }); // 1424
} //
//
/// //
/// CLEAN UP FOR `logoutOtherClients` //
/// //
//
Ap._deleteSavedTokensForUser = function (userId, tokensToDelete) { // 1431
if (tokensToDelete) { // 1432
this.users.update(userId, { // 1433
$unset: { // 1434
"services.resume.haveLoginTokensToDelete": 1, // 1435
"services.resume.loginTokensToDelete": 1 // 1436
}, //
$pullAll: { // 1438
"services.resume.loginTokens": tokensToDelete // 1439
} //
}); //
} //
}; //
//
Ap._deleteSavedTokensForAllUsersOnStartup = function () { // 1445
var self = this; // 1446
//
// If we find users who have saved tokens to delete on startup, delete //
// them now. It's possible that the server could have crashed and come //
// back up before new tokens are found in localStorage, but this //
// shouldn't happen very often. We shouldn't put a delay here because //
// that would give a lot of power to an attacker with a stolen login //
// token and the ability to crash the server. //
Meteor.startup(function () { // 1454
self.users.find({ // 1455
"services.resume.haveLoginTokensToDelete": true // 1456
}, { //
"services.resume.loginTokensToDelete": 1 // 1458
}).forEach(function (user) { //
self._deleteSavedTokensForUser(user._id, user.services.resume.loginTokensToDelete); // 1460
}); //
}); //
}; //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/accounts-base/accounts_rate_limit.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
var Ap = AccountsCommon.prototype; // 1
var defaultRateLimiterRuleId; // 2
// Removes default rate limiting rule //
Ap.removeDefaultRateLimit = function () { // 4
var resp = DDPRateLimiter.removeRule(defaultRateLimiterRuleId); // 5
defaultRateLimiterRuleId = null; // 6
return resp; // 7
}; //
//
// Add a default rule of limiting logins, creating new users and password reset //
// to 5 times every 10 seconds per connection. //
Ap.addDefaultRateLimit = function () { // 12
if (!defaultRateLimiterRuleId) { // 13
defaultRateLimiterRuleId = DDPRateLimiter.addRule({ // 14
userId: null, // 15
clientAddress: null, // 16
type: 'method', // 17
name: function (name) { // 18
return _.contains(['login', 'createUser', 'resetPassword', 'forgotPassword'], name); // 19
}, //
connectionId: function (connectionId) { // 22
return true; // 23
} //
}, 5, 10000); //
} //
}; //
//
Ap.addDefaultRateLimit(); // 29
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/accounts-base/url_server.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// XXX These should probably not actually be public? //
//
AccountsServer.prototype.urls = { // 3
resetPassword: function (token) { // 4
return Meteor.absoluteUrl('#/reset-password/' + token); // 5
}, //
//
verifyEmail: function (token) { // 8
return Meteor.absoluteUrl('#/verify-email/' + token); // 9
}, //
//
enrollAccount: function (token) { // 12
return Meteor.absoluteUrl('#/enroll-account/' + token); // 13
} //
}; //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
(function(){
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/accounts-base/globals_server.js //
// //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
/** //
* @namespace Accounts //
* @summary The namespace for all server-side accounts-related methods. //
*/ //
Accounts = new AccountsServer(Meteor.server); // 5
//
// Users table. Don't use the normal autopublish, since we want to hide //
// some fields. Code to autopublish this is in accounts_server.js. //
// XXX Allow users to configure this collection name. //
//
/** //
* @summary A [Mongo.Collection](#collections) containing user documents. //
* @locus Anywhere //
* @type {Mongo.Collection} //
*/ //
Meteor.users = Accounts.users; // 16
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}).call(this);
/* Exports */
if (typeof Package === 'undefined') Package = {};
Package['accounts-base'] = {
Accounts: Accounts,
AccountsServer: AccountsServer,
AccountsTest: AccountsTest
};
})();
//# sourceMappingURL=accounts-base.js.map