29 KiB
account-base package
Files documented
- accounts_client.js
- accounts_common.js
- accounts_server.js
- accounts_rate_limit.js
- globals_client.js
- globals_server.js
- package.js
- url_client.js
- url_server.js
Not documented: localstorage_token.js is supposed to be an opaque implementation detail.
Architecture
The accounts system is founded on the users collection and the notion of login tokens, which allow reconnecting.
Connections have observers based on the Mongo implementation.
Collections
users. Indexes (other indexes are added by other packages, likeaccount-password:('username', {unique: 1, sparse: 1});('emails.address', {unique: 1, sparse: 1});('services.resume.loginTokens.hashedToken', {unique: 1, sparse: 1});('services.resume.loginTokens.token', {unique: 1, sparse: 1});('services.resume.haveLoginTokensToDelete', { sparse: 1 });: For taking care of logoutOtherClients calls that crashed before the tokens were deleted."services.resume.loginTokens.when", { sparse: 1 });: For expiring login tokens
Constants
DEFAULT_LOGIN_EXPIRATION_DAYS= 90. Default login token lifetime. Used byAccountsCommon_getTokenLifetimeMs().MIN_TOKEN_LIFETIME_CAP_SECS= 3600. Maximum value of "soon". Used byAccountsCommon._tokenExpiresSoon(when).EXPIRE_TOKENS_INTERVAL_MS= 100000. Frequency of token expiration checks. Used bysetExpireTokensInterval(accounts)inaccounts_server.js.CONNECTION_CLOSE_DELAY_MS= 10000. Logout delay for other clients. Used byMeteor.logoutOtherClients(), added fromaccounts_server.js.
Classes
AccountsClient extends AccountsCommon (accounts_client.js)
constructor(options)- `options``:
connection: optional DDP connection to reuseddpUrl: optional DDP URL to create a new connection
- calls parent constructor
- initializes properties:
_loggingIn: "private", useloggingIn()/setLoggingIn()for reactivity_loggingInDeps: Tracker dependency_loginServicesHandle: subscription tometeor.loginServiceConfiguration_pageLoadLoginCallbacks: an array of page loading callbacks_pageLoadLoginAttemptInfo: holds the information passed to login callbacks during a page-load login attempt
- invokes
_initUrlMatching()(fromurl_client.js) - invokes
_initLocalStorage()(fromlocalstorage_token.js)
- `options``:
callLoginMethod(options): call login method on the server- A login method is a method which on success calls
this.setUserId(id)andAccounts._setLoginTokenon the server and returns an object with fields 'id' (actuallyuserId(containing the user id), 'token' (containing a resume token), and optionallytokenExpires. - This function takes care of:
- Updating the
Meteor.loggingIn()reactive data source - Calling the method in
waitmode - On success, saving the resume token to localStorage
- On success, calling
Accounts.connection.setUserId() - Setting up an
onReconnecthandler which logs in with the resume token - Running
onLoginHookoronLoginFailureHookthen theuserCallbackoption
- Updating the
- Options:
methodName: The method to call (defaultlogin)methodArguments: The arguments for the methodvalidateResult: If provided, will be called with the result of the method. If it throws, the client will not be logged in (and its error will be passed to the callback).userCallback: Will be called with no arguments once the user is fully logged in, or with a single-element array containing the error on error.
- A login method is a method which on success calls
loggingIn(): reactive getter for_loggingInloginServicesConfigured(): A reactive function returning whether theloginServiceConfigurationsubscription is ready. Used byaccounts-uito hide the login button until we have all the configuration loadedlogout(callback): logs the user out:- call
connection.logout() - clear token
- invoke callback
- call
logoutOtherClients(callback): Log out other clients logged in as the current user, but does not log out the client that calls this function. Interesting logic to execute two method calls without having the event loop run between them.makeClientLoggedIn(): helper forcallLoginMethod()makeClientLoggedOut(): helper forlogout()and reconnect errorsonPageLoadLogin(f): Register a callback to be called if we have information about a login attempt at page load time. Call the callback immediately if we already have the page load login attempt info, otherwise stash the callback to be called if and when we do get the attempt info.userId()overrides pseudo-abstract parent method to use the per-connection user id._pageLoadLogin(attemptInfo):_setLoggingIn(): setter for_loggingIn(triggers reactive update)url_client.jsadditions:_attemptToMatchHash(): Try to match the saved value of window.location.hash to one of the reserved hashes, to trigger an Accounts operation. On success, invokes passed handler which, when called from_initUrlMatching(), will always bedefaultSuccessHandler()._initUrlMatching(): called by constructor. Inits extra data on instance and invokes_attemptToMatchHash()onResetPasswordLink()Register a function to call when a reset password link is clicked in an email sent by on of the hash handlers. See Accounts-onResetPasswordLinkonEmailVerificationLink()Register a function to call when an email verification link is clicked in an email sent by a hash handler. See Accounts-onEmailVerificationLinkonEnrollmentLink()Register a function to call when an account enrollment link is clicked in an email sent by a hash handler. See Accounts-onEnrollmentLink- Globals read
window.location.hash. Reserved hashes:reset-passwordverify-emailenroll-account
defaultSuccessHandler()attemptToMatchHash()
AccountsCommon (accounts_common.js)
Base class for AccountsClient / AccountsServer.
constructor(options).- initializes
connection, thenusers. - Options can contain:
connection,ddpUrlseeinitConnection()sendVerificationEmail,forbidClientAccountCreation,restrictCreationByEmailDomain,loginExpirationInDays, andoauthSecretKey(side-effect, not saved). seeconfig(options)
- initializes
addDefaultRateLimit(): enable per-connection, per-method rate limiter forlogin,createUser,resetPasswordforgotPasswordto 5 calls every 10 seconds. Added fromaccounts_rate_limits.js.config(options). Set up config for the accounts system. Call this on both the client the server. Overridden in server.- Checks and filters options, before saving them to
_options. - Setting an unknown option throws
- Setting an already set option throws
- Options can contain:
sendVerificationEmail{Boolean}: Send email address verification emails to new users created from client signups.forbidClientAccountCreation{Boolean} Do not allow clients to create accounts directly. Security issue #828 exists if this is not called on both client and serverrestrictCreationByEmailDomain{Function or String} Require created users to have an email accepted by the function (by returning trueish) or having the string match the domain as a case-insensitive fully-anchored regex. SeedefaultValidateNewUserHook().loginExpirationInDays{Number} Number of days since login until a user is logged out (login token expires).oauthSecretKeyWhen using theoauth-encryptionpackage, the 16 byte key using to encrypt sensitive account credentials in the database, encoded in base64.- Warns if the
oauth-encryptionpackage is not present - Throws if used on client
- Removed from saved config after passing if to the
oauth-encryptionpackage
- Warns if the
- Checks and filters options, before saving them to
ConfigError: legacy, initialized fromservice-configurationpackage duringMeteor.startup().connection: the MongoDB connection. If set to null, theuserscollection will be local (avoid !)LoginCancelledError: specific error class to use when a login sequence is cancelledloginServiceConfiguration: legacy, initialized fromservice-configurationpackage duringMeteor.startup().removeDefaultRateLimit(): disable the rate limiter for the methods below (fromaccounts_rate_limits.js).user(): returns the currently logged-in user by finding it from Mongo based on theuserId()value. Defaults tonull.userId():Error("userId method not implemented")Basically an abstract method to be refined in child classesusers: the users collectiononLogin(func): Register a callback to be called after a login attempt succeeds.onLoginFailure(func): Register a callback to be called after a login attempt fails._getTokenLifetimeMs(): get the remaining login token lifetime in msec. Taken fromloginExpirationInDaysif it exists. Defaults toDEFAULT_LOGIN_EXPIRATION_DAYS(= 90) days in msec._initConnection(options)- Options can containconnection: the connection on which to load theuserscollectionddpUrl: if connection is not set, connect to this URL- some non-portable, going-away, mechanism for OAuth
- if none if available,
Meteor.connectionwill be used as a default
_onLoginHook(). As per hook.js, Hook system is under development. UseonLogin(func)to make use of it._onLoginFailureHook(). As per hook.js, Hook system is under development. UseonLoginFailure(func)to make use of it._options = {}- used directly by packages likeaccounts-passwordand `accounts-ui-unstyled._tokenExpiration(when):whenis a token (timestamp, used to be any number in earlier versions). It is converted to Date, and added with_getTokenLifetimeMs()to return the expiration date for thewhen._tokenExpiresSoon(when):whenis a token (timestamp). True if it expires in less the smaller of0.1 * _getTokenLifetimeMs()and 1 hour.- side-effect in
accounts_rate_limits.js: loading this file initializes the rate-limiter foraddDefaultRateLimit()andremoveDefaultRateLimit(). This is why the package has a dependency onddp-rate-limiter.
AccountsServer extends AccountsCommon (accounts_server.js)
-
constructor(server)- invokes the
AccountsCommonconstructor - initializes
_serverfrom the server argument if not empty, defaulting toMeteor.serverotherwise - initializes methods using
_initServerMethods() - initializes
_accountDatausing_initAccountDataHooks() - if
autopublishis present, mark user fields as published:- for everyone:
profileandusername - for current user only:
emails
- for everyone:
- publishes account system collections using
_initServerPublications() - sets up the
userscollection usingsetupUsersCollection(users). - sets up the default login handler using
setupDefaultLoginHandlers(). - sets up token expiration using
setExpireTokensInterval(accounts). - initializes
_validateLoginHook - initializes
_validateNewUserHooksto the singledefaultValidateNewUserHook(). - deletes saved tokens for all users using
_deleteSavedTokensForAllUsersOnStartup() - initializes
_skipCaseInsensitiveChecksForTest: used by tests only.
- invokes the
-
addAutopublishFields(opts): allow packages to declare extra fields when autopublish is active. -
config(options): overrides parent method, but starts by invoking it. Option:loginExpirationInDays: removes the background token expiration observer
-
destroyToken(userId, loginToken): Deletes the given loginToken from the database. For new-style hashed token, this will cause all connections associated with the token to be closed. -
defaultResumeLoginHandler(accounts, options): Login handler for resume tokens. The token is found inoptions.resume. -
defaultValidateNewUserHook(user): Validate new user's email or Google/Facebook/GitHub account's email, if therestrictCreationByEmailDomainconfig option is active. -
insertUserDoc(options, user): main user account creation function, used byaccounts-password:- 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)
- invoke the user create callback (hook) and validateNewUser hooks, throwing a 403 on any validation fail.
- insert the document in the collection, throwing a 403 on some insertion errors
- Apparently not completely stable yet. From doc:
- 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)
- XXX better error reporting for services.facebook.id duplicate, etc
-
onCreateUser(func): registersfuncas the single user creation hook allowed. -
registerLoginHandler(name, handler): The main entry point for auth packages to hook in to login. A login handler is a login method which can returnundefinedto indicate that the login request is not handled by this handler.name{String} Optional. The service name, used by default if a specific service name isn't returned in the result.handler{Function} A function that receives an options object (as passed as an argument to theloginmethod) and returns one of:undefined, meaning don't handle; or a login method result object as described on_loginUser.
-
setExpireTokensInterval(accounts): starts a low-frequency (EXPIRE_TOKENS_INTERVAL_MS= 10 sec) task expiring tokens. Can be deactivated usingconfig({ loginExpirationInDays: null }). -
setupDefaultLoginHandlers(): registersdefaultResumeLoginHandler()as a login handler calledresumeusingregisterLoginHandler(). -
setupUsersCollection(users): configures theuserscollection obtained from the parent constructor, by applyingusers.allowto limite update rights to the document for the current user, and ensuring multiple MongoDB indexes:username:{unique: 1, sparse: 1})emails.address:{ unique: 1, sparse: 1})services.resume.loginTokens.hashedToken:{unique: 1, sparse: 1})services.resume.loginTokens.token:{ unique: 1, sparse: 1})services.resume.haveLoginTokensToDelete:{ sparse: 1 })services.resume.loginTokens.when:{ sparse: 1 })
-
updateOrCreateUserFromExternalService(serviceName, serviceData, options): 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. (Side note: there is a specific kludge for old Twitter ids).@param options{Object, optional} Other options to pass to insertUserDoc (eg, profile)@returns{Object} Object withtokenandid(actuallyuserId) keys, like the result of theloginmethod.- "internal" services
resumeandpasswordmay not use this - does NOT update
profilebut updatesserviceData - Not completely stable. Per docs: XXX provide an onUpdateUser hook which would let apps update the profile too
-
userId(): overrides the unimplemented version inAccountsCommon. This function only works if called inside a method, throws otherwise. -
usingOAuthEncryption(): is OAuth encryption present AND is a key loaded ? -
validateLoginAttempt(func): registersfuncas a login attempt validation hook, returning an object with astop()method to unregister it. -
validateNewUser(func): registerfuncas a user account creation validation hook, not returning anything. -
_accountData: seesetAccountData(). -
_attemptLogin(methodInvocation, methodName, methodArgs, result): After a login method has completed, call the login hooks: validation (which can turn allowed into disallowed), and login or loginFailure hooks. Note thatattemptLoginis 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. Use_loginMethod()instead. -
_clearAllLoginTokens(userId): removes all login tokens on a user identified by `userId``. -
_deleteSavedTokensForAllUsersOnStartup(): onMeteor.startup(), immediately clean discovered saved tokens which applied to the previous instance of the application. -
_deleteSavedTokensForUser(userId, tokensToDelete): used by the delayed logout (CONNECTION_CLOSE_DELAY_MS) in obsolete methodlogoutOtherClients()and helper for_deleteSavedTokensForAllUsersOnStartup()to logout other clients. -
_expireTokens(oldestValidDate, userId): 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. Side-effect The observe on Meteor.users will take care of closing connections for expired tokens. -
_failedLogin(): invokes theAccountsCommon.onLoginFailureHookimplementations. -
_generateStampedLoginToken(): generates a pseudo-random login token. As per docs: "Used by Meteor Accounts server and tests". -
_getAccountData(connectionId, field): get the login token for a connection. Documented as a "HACK: This is used by 'meteor-accounts' to get the loginToken for a connection. Maybe there should be a public way to do that.". Also used by_getLoginToken(). -
_getLoginToken(connectionId): gets the login token for a connection, using the_getAccountData()hack. -
_getUserObserve(connectionId): test helper - returns the user observed on a connection -
_hashLoginToken(loginToken): hashes a token with sha256, returning the hash in base64 encoding. -
_hashStampedToken(stampedToken): modifies a stamped token from{ token: ..., ... }to{ hashedtoken: ..., ... } -
_initAccountDataHooks(): add a DDPonConnectcallback which- stores the connection in
_accountData[connection.id] - add a DDP
onClosecallback removing the current user token from the connection using_removeTokenFromConnection(connection.id);and removing the connection stored in_accountData[connection.id]on connection.
- stores the connection in
-
_initServerMethods(): adds the account-related methods defined by the class. See_server._methodHandlers. -
_initServerPublications(): starts account-related publications:meteor.loginServiceConfigurationfrom the (OAuth) service configuration. Thesecretfield is removed from the publication, hence it is only available when querying server-side.userscursor publish:- no autopublish and logged: just fields
profile,username, andemailsfor the current user if applicable, - no autopublish and not logged:
null - autopublish and logged: all autopublished fields for current user, including those added by other packages using
addAutopublishFields(forLoggedInUser) - autopublish and not logged:
_idandusername(if available) for all users, and fields added by other packages usingaddAutopublishFields(forOtherUsers)
- no autopublish and logged: just fields
usersanonymous with more fields,
-
_insertHashedLoginToken(userId, hashedToken, query): add a hashed token to theservices.resume.loginTokensarray on a user account. Do (incorrectly ?) says an index error can be thrown if the token is already present (as per MongoDB docs, it just does nothing) -
_insertLoginToken(userId, stampedToken, query): test helper combining_insertHashedLoginToken()and_hashStampedToken() -
_loginHandlers: list of all registered handlers. Starts as[]. -
_loginMethod(methodInvocation, methodName, methodArgs, type, fn)the safe version of_attemptLogin()is the one to use to login a user. -
_loginUser(methodInvocation, userId, stampedLoginToken): Log in a user on a connection after success. Returns{ id: userId, token: stampedLoginToken.token, tokenExpires: self._tokenExpiration(stampedLoginToken.when) };. Invoked from_attemptLogin(). -
_nextUserObserveNumber: the next observer number for_userObservesForConnections. -
_removeTokenFromConnection(connectionId): remove an observer token from a connection. -
_reportLoginFailure(methodInvocation, methodName, methodArgs, result): 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. -
_runLoginHandlers(methodInvocation, options): 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. Works by invokingtryLoginMethod()on each handler until one returns something not undefined. Throws Error 400 on incorrect options for a handler, or a handler returning neither undefined nor a result object as described on_loginUser. -
_server: aServer(fromddp-server/livedata_server.js) instance. Notable properties in this context:_methodHandlers: the Meteor methods tablelogin(options): runsresult = _runLoginHandlers(self, options); _attemptLogin(self, "login", arguments, result);. The method ensuresoptionsis an object, but it is up to login handlers to check whatever field they look at in options: Meteor doesn't check them.logout(): deletes the login token for the connection withdestroyTokenand clears the current user id using methodsetUserId(null).logoutOtherClients(): obsolete for compatibility with 0.7.2, usegetNewToken()andremoveOtherTokens()insteadgetNewToken(): 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.removeOtherTokens(): 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.configureLoginService(options): Allow a one-time configuration for a login service. Modifications to this collection are also allowed in insecure mode. Option keys:service: String, required. The name of the service to configure. Bug : this method can only be used to configure OAuth login providers. There is a XXX in the code about this problem having to be fixed, either by moving use ofserviceto theaccounts-oauthpackage, or to move the list of services fromaccounts-oauthtoaccounts-base
-
_setAccountData(connectionId, field, value): modifies or deletes data from a connection kept on_accountData. -
_setLoginToken(userId, connection, newToken): replaces any token for a user on a connection with a new token. If the latter is not empty, sets up an observer for the token. -
_successfulLogin(): invokes theAccountsCommon.onLoginHookimplementations. -
_testEmailDomain(email): checks user email matches the domain configured in therestrictCreationByEmailDomainconfig option. Used bydefaultValidateNewUserHook(). -
_userObservesForConnections: 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). Numbers obtained from_nextUserObserveNumber. -
_validateLogin(connection, attempt): perform login validation using available implementations in_validateLoginHook. Note that all validators run even if one or more of them denies access or throws an error. The first reported error is the one the user receives by default, but later validators may override this behavior. Invoked from_attemptLogin(). -
_validateLoginHook: holds a login validation hook. Doc inhook.jswarns "This pattern is under development. Do not add more callsites using this package for now" -
_validateNewUserHooks: holds an array of new user hooks. -
note about methods. These 3 methods are public but marked (in 1.2.1) as likely not to remain so:
resetPassword(): generates a password reset link (from token)verifyEmail(): generates an email verification link (from token)enrollAccount(): generates an account enrollment link (from token)
AccountsTest
- methods
attemptToMatchHash()facade forattemptToMatchHash()function- Globals read
Accounts(seeglobals_server.js)
Meteor
userId: a copy of theAccounts.usedId()methoduser(): a copy of theAccounts.user()method
Functions
accounts_server.js
defaultCreateUserHook(options, user): _XXX see comment on Accounts.createUser in passwords_server about adding a second "server options" argument.defaultValidateNewUserHook()a weak email validation method based on the domain name. SeeAccountsServer._validateNewUserHooks.cloneAttemptWithConnection(connection, attempt)clone the attempt object, preserving the connection inside it instead of cloning it toopinEncryptedFieldsToUser(serviceData, userId): 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.tryLoginMethod(type, fn): Try a login method, converting thrown exceptions into an {error} result. Thetypeargument is a default, inserted into the result object if not explicitly returned.
url_client.js
defaultSuccessHandler(): suspends autologin, invokes other handles for the same hash, passing them a closure capable of enabling autologin.
Dependencies / Exports (package.js et al.)
Exports
| Symbol | Client | Server | Test |
|---|---|---|---|
| Accounts | O | O | O |
| AccountsClient | O | ||
| AccountsServer | O | ||
| AccountsTest | O |
- Exposes
Accounts- on client:
new AccountsClient()(extendsAccountsCommon) - on server:
new AccountsServer(Meteor.server)(extendsAccountsCommon)
- on client:
- Modifies
Meteor- new field
usersfor theuserscollection. Name is expected to become configurable in future versions. - on client:
Meteor.loggingIn()is an alias forAccounts.loggingIn(). - on client:
Meteor.logout()is an alias forAccountsClient.logout(). - on client:
Meteor.logoutOtherClients()is an alias forAccountsClient.logoutOtherClients().
- new field
- Template helpers (with Blaze / Spacebars only):
currentUser->Meteor.user().loggingIn->Meteor.loggingIn().
Dependencies
| Package | Client | Server | Specifics |
|---|---|---|---|
| underscore | O | O | |
| ecmascript | O | O | |
| ddp-rate-limiter | O | O | |
| localstorage | O | ||
| tracker | O | ||
| check | O | ||
| random | O | O | |
| ejson | O | ||
| callback-hook | O | O | |
| service-configuration | O | O | unordered (needs Accounts.connection) |
| ddp | O | O | |
| mongo | O | O | expected abstraction in the future |
| blaze | O | weak: define {{currentUser}} | |
| autopublish | O | weak: publish extra users fields | |
| oauth-encryption | O | weak | |
| NPM crypto | O | in accounts_server.js |
Package-local variables
accounts-server.js
OAuthEncryption: if packageoauth-encryptionis present, contains itsencryptionexport. Used by OAuth-related functions in this package.
Side-effects
If OAuth encryption is present and a key is loaded, the startup code in accounts_server.js seals the secret field in ServiceConfiguration exported by the service-configuration package. This appears to be unstable, as per docs: 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?