var _ = require('underscore'); var path = require('path'); var fs = require('fs'); var utils = require('./utils.js'); var files = require('./files.js'); var config = require('./config.js'); var httpHelpers = require('./http-helpers.js'); var archinfo = require('./archinfo.js'); var fiberHelpers = require('./fiber-helpers.js'); var release = require('./release.js'); var querystring = require('querystring'); var url = require('url'); var Future = require('fibers/future'); var isopackets = require('./isopackets.js'); var Console = require('./console.js').Console; var auth = exports; var getLoadedPackages = function () { return isopackets.load('ddp'); }; // Opens and returns a DDP connection to the accounts server. Remember // to close it when you're done with it! var openAccountsConnection = function () { var DDP = getLoadedPackages().ddp.DDP; return DDP.connect(config.getAuthDDPUrl(), { headers: { 'User-Agent': httpHelpers.getUserAgent() } }); }; // Returns a function that runs `f`, appending an additional argument // that is a connection to the accounts server, which gets closed when // `f` returns or throws. var withAccountsConnection = function (f) { return function (/* arguments */) { var self = this; var args = _.toArray(arguments); var conn = openAccountsConnection(); args.push(conn); try { var result = f.apply(self, args); } finally { conn.close(); } return result; }; }; // Open a DDP connection to the accounts server and log in using the // provided token. Returns the connection, or null if login fails. // // XXX if we reconnect we won't reauthenticate. Fix that before using // this for long-lived connections. var loggedInAccountsConnection = function (token) { var connection = getLoadedPackages().ddp.DDP.connect( config.getAuthDDPUrl() ); var fut = new Future; connection.apply( 'login', [{ resume: token }], { wait: true }, function (err, result) { fut['return']({ err: err, result: result }); } ); var outcome = fut.wait(); if (outcome.err) { connection.close(); if (outcome.err.error === 403) { // This is not an ideal value for the error code, but it means // "server rejected our access token". For example, it expired // or we revoked it from the web. return null; } // Something else went wrong throw outcome.err; } return connection; }; // The accounts server has some wrapped methods that take and return // session identifiers. To call these methods, we add our current // session identifier (or null, if we don't have one) as the last // argument to the method. The accounts server returns an object with // keys 'result' (the actual method result) and 'session' (the new // session identifier we should use, if it created a new session for // us). // options can include: // - timeout: a timeout after which an exception will be thrown if the // method hasn't returned yet // - connection: an open connection to the accounts server. If not // provided, one will be opened and then closed before returning. var sessionMethodCaller = function (methodName, options) { options = options || {}; return function (/* arguments */) { var args = _.toArray(arguments); args.push({ session: auth.getSessionId(config.getAccountsDomain()) || null }); var fut = new Future(); var conn = options.connection || openAccountsConnection(); conn.apply(methodName, args, fiberHelpers.firstTimeResolver(fut)); if (options.timeout !== undefined) { var timer = setTimeout(fiberHelpers.bindEnvironment(function () { if (!fut.isResolved()) fut.throw(new Error('Method call timed out')); }), options.timeout); } try { var result = fut.wait(); } finally { if (timer) { clearTimeout(timer); } if (! options.connection) conn.close(); } if (result && result.session) { auth.setSessionId(config.getAccountsDomain(), result.session); } return result && result.result; }; }; var readSessionData = function () { var sessionPath = config.getSessionFilePath(); if (! fs.existsSync(sessionPath)) return {}; return JSON.parse(fs.readFileSync(sessionPath, { encoding: 'utf8' })); }; var writeSessionData = function (data) { var sessionPath = config.getSessionFilePath(); var tries = 0; while (true) { if (tries++ > 10) throw new Error("can't find a unique name for temporary file?"); // Create a temporary file in the same directory where we // ultimately want to write the session file. Use the exclusive // flag to atomically ensure that the file doesn't exist, create // it, and make it readable and writable only by the current // user (mode 0600). var tempPath = path.join(path.dirname(sessionPath), '.meteorsession.' + Math.floor(Math.random() * 999999)); try { var fd = fs.openSync(tempPath, 'wx', 0600); } catch (e) { continue; } try { // Write `data` to the file. var buf = new Buffer(JSON.stringify(data, undefined, 2), 'utf8'); fs.writeSync(fd, buf, 0, buf.length, 0); } finally { fs.closeSync(fd); } // Atomically remove the old file (if any) and replace it with // the temporary file we just created. fs.renameSync(tempPath, sessionPath); return; } }; var getSession = function (sessionData, domain) { if (typeof (sessionData.sessions) !== "object") sessionData.sessions = {}; if (typeof (sessionData.sessions[domain]) !== "object") sessionData.sessions[domain] = {}; return sessionData.sessions[domain]; }; // types: // - "meteor-account": a login to your Meteor Account // - "galaxy": a login to a Galaxy var ensureSessionType = function (session, type) { if (! _.has(session, 'type')) session.type = type; else if (session.type !== type) { // Blow away whatever was there. We lose pendingRevokes but that's // OK since this should never happen in normal operation. (It // would happen if the Meteor Accounts server mode somewhere else // and a Galaxy was deployed at its old address, for example). _.each(_.keys(session), function (key) { delete session[key]; }); session.type = type; } }; var writeMeteorAccountsUsername = function (username) { var data = readSessionData(); var session = getSession(data, config.getAccountsDomain()); session.username = username; writeSessionData(data); }; // Given an object 'data' in the format returned by readSessionData, // modify it to make the user logged out. var logOutAllSessions = function (data) { _.each(data.sessions, function (session, domain) { logOutSession(session); }); }; // As logOutAllSessions, but for a session on a particular domain // rather than all sessions. var logOutSession = function (session) { var crypto = require('crypto'); delete session.username; delete session.userId; delete session.registrationUrl; if (_.has(session, 'token')) { if (! (session.pendingRevoke instanceof Array)) session.pendingRevoke = []; // Delete the auth token itself, but save the tokenId, which is // useless for authentication. The next time we're online, we'll // send the tokenId to the server to revoke the token on the // server side too. if (typeof session.tokenId === "string") session.pendingRevoke.push(session.tokenId); delete session.token; delete session.tokenId; } }; // Given an object 'data' in the format returned by readSessionData, // return true if logged in, else false. var loggedIn = function (data) { return !! getSession(data, config.getAccountsDomain()).userId; }; // Given an object 'data' in the format returned by readSessionData, // return the currently logged in user, or null if not logged in or if // the logged in user doesn't have a username. var currentUsername = function (data) { var sessionData = getSession(data, config.getAccountsDomain()); return sessionData.username || null; }; var removePendingRevoke = function (domain, tokenIds) { var data = readSessionData(); var session = getSession(data, domain); session.pendingRevoke = _.difference(session.pendingRevoke, tokenIds); if (! session.pendingRevoke.length) delete session.pendingRevoke; writeSessionData(data); }; var tryRevokeGalaxyTokens = function (domain, tokenIds, options) { var oauthInfo = fetchGalaxyOAuthInfo(domain, options.timeout); if (oauthInfo) { url = oauthInfo.revokeUri; } else { return false; } try { var result = httpHelpers.request({ url: url, method: "POST", form: { tokenId: tokenIds.join(',') }, useSessionHeader: true, timeout: options.timeout }); } catch (e) { // most likely we don't have a net connection return false; } var response = result.response; if (response.statusCode === 200 && response.body) { try { var body = JSON.parse(response.body); if (body.tokenRevoked) { // Server confirms that the tokens have been revoked. Checking for a // `tokenRevoked` key in the response confirms that we hit an actual // galaxy auth server that understands that we were trying to revoke some // tokens, not just a random URL that happened to return a 200 // response. // (Be careful to reread session data in case httpHelpers changed it) removePendingRevoke(domain, tokenIds); } } catch (e) { return false; } return true; } else { return false; } }; // If there are any logged out (pendingRevoke) tokens that haven't // been sent to the server for revocation yet, try to send // them. Reads the session file and then writes it back out to // disk. If the server can't be contacted, fail silently (and leave // the pending invalidations in the session file for next time). // // options: // - timeout: request timeout in milliseconds // - firstTry: cosmetic. set to true if we recently logged out a // session. just changes the error message. // - connection: an open connection to the accounts server. If not // provided, this function will open one itself. var tryRevokeOldTokens = function (options) { options = _.extend({ timeout: 5000 }, options || {}); var warned = false; var domainsWithRevokedTokens = []; _.each(readSessionData().sessions || {}, function (session, domain) { if (session.pendingRevoke && session.pendingRevoke.length) domainsWithRevokedTokens.push(domain); }); var logoutFailWarning = function (domain) { if (! warned) { // This isn't ideal but is probably better that saying nothing at all Console.error("warning: " + (options.firstTry ? "couldn't" : "still trying to") + " confirm logout with " + domain); warned = true; } }; _.each(domainsWithRevokedTokens, function (domain) { var data = readSessionData(); var session = data.sessions[domain] || {}; var tokenIds = session.pendingRevoke || []; if (! tokenIds.length) return; var url; if (session.type === "meteor-account") { try { sessionMethodCaller('revoke', { timeout: options.timeout, connection: options.connection })(tokenIds); removePendingRevoke(domain, tokenIds); } catch (err) { logoutFailWarning(domain); } return; } else if (session.type === "galaxy") { if (! tryRevokeGalaxyTokens(domain, tokenIds, options)) { logoutFailWarning(domain); } } else { // don't know how to revoke tokens of this type logoutFailWarning(domain); return; } }); }; // Sends a request to https://: to find out the // galaxy's OAuth client id and redirect_uri that should be used for // authorization codes for this galaxy. Returns an object with keys // 'oauthClientId', 'redirectUri', and 'revokeUri', or null if the // request failed. // // 'timeout' is an optional request timeout in milliseconds. var fetchGalaxyOAuthInfo = function (galaxyName, timeout) { var galaxyAuthUrl = 'https://' + galaxyName + ':' + config.getDiscoveryPort() + '/_GALAXYAUTH_'; try { var result = httpHelpers.request({ url: galaxyAuthUrl, json: true, // on by default in our version of request, but just in case strictSSL: true, followRedirect: false, timeout: timeout || 5000 }); } catch (e) { return null; } if (result.response.statusCode === 200 && result.body && result.body.oauthClientId && result.body.redirectUri && result.body.revokeUri) { return result.body; } else { return null; } }; var sendAuthorizeRequest = function (clientId, redirectUri, state) { var authCodeUrl = config.getOauthUrl() + "/authorize?" + querystring.stringify({ state: state, response_type: "code", client_id: clientId, redirect_uri: redirectUri }); // It's very important that we don't have request follow the // redirect for us, but instead issue the second request ourselves, // since request would pass our credentials along to the redirected // URL. See comments in http-helpers.js. var codeResult = httpHelpers.request({ url: authCodeUrl, method: 'POST', strictSSL: true, useAuthHeader: true }); var response = codeResult.response; if (response.statusCode !== 302 || ! response.headers.location) { throw new Error('access-denied'); } if (url.parse(response.headers.location).hostname !== url.parse(redirectUri).hostname) { // If we didn't get an immediate redirect to the redirectUri then // presumably the oauth server is trying to interact with us (make // us log in, authorize the client, or something like that). We're // not a web browser so we can't participate in such things. throw new Error('access-denied'); } return { location: response.headers.location }; }; // Do an OAuth flow with the Meteor developer accounts server to log in // to an OAuth client. `conn` is expected to be a DDP connection to the // OAuth client app. Options are: // - clientId: OAuth client id parameter // - redirectUri: OAuth redirect_uri parameter // - domain: the domain for saving the received login token on success // in the Meteor session file // - sessionType: the value of the 'type' field for the session saved // in the Meteor session file on success // All options are required. // // Throws an error if the login is not successful. var oauthFlow = function (conn, options) { var crypto = require('crypto'); var credentialToken = crypto.randomBytes(16).toString('hex'); var authorizeResult = sendAuthorizeRequest( options.clientId, options.redirectUri, credentialToken ); // XXX We're using a test-only flag here to just get the raw // credential secret (instead of a bunch of code that communicates the // credential secret somewhere else); this should be temporary until // we give this a nicer name and make it not just test only. var redirectResult = httpHelpers.request({ url: authorizeResult.location + '&only_credential_secret_for_test=1', method: 'GET', strictSSL: true }); var response = redirectResult.response; // 'access-denied' isn't exactly right because it's possible that the server // went down since our last request, but close enough. if (response.statusCode !== 200) { throw new Error('access-denied'); } // XXX tokenId??? var loginResult = conn.apply('login', [{ oauth: { credentialToken: credentialToken, credentialSecret: response.body } }], { wait: true }); if (loginResult.token && loginResult.id) { var data = readSessionData(); var session = getSession(data, options.domain); ensureSessionType(session, options.sessionType); session.token = loginResult.token; writeSessionData(data); return true; } else { throw new Error('login-failed'); } }; // Uses meteor accounts to log in to the specified galaxy. Returns an // object with keys `token` and `tokenId` if the login was // successful. If an error occurred, returns one of: // { error: 'access-denied' } // { error: 'no-galaxy' } // { error: 'no-account-server' } var logInToGalaxy = function (galaxyName) { var oauthInfo = fetchGalaxyOAuthInfo(galaxyName); if (! oauthInfo) { return { error: 'no-galaxy' }; } var galaxyClientId = oauthInfo.oauthClientId; var galaxyRedirect = oauthInfo.redirectUri; // If the redirect URI is not in the DNS namespace that belongs to the // Galaxy, then something is wrong. if (url.parse(galaxyRedirect).hostname !== galaxyName) { // XXX It's more like 'bad-galaxy' than 'no-galaxy'. return { error: 'no-galaxy' }; } // Ask the accounts server for an authorization code. var crypto = require('crypto'); var session = crypto.randomBytes(16).toString('hex'); var stateInfo = { session: session }; var authorizeResult; try { authorizeResult = sendAuthorizeRequest( galaxyClientId, galaxyRedirect, encodeURIComponent(JSON.stringify(stateInfo)) ); } catch (err) { return { error: err.message }; } // Ask the galaxy to log us in with our auth code. try { var galaxyResult = httpHelpers.request({ url: authorizeResult.location, method: 'GET', strictSSL: true, headers: { cookie: 'GALAXY_OAUTH_SESSION=' + session + '; GALAXY_USER_AGENT_TOOL=' + encodeURIComponent(JSON.stringify(utils.getAgentInfo())) } }); var body = JSON.parse(galaxyResult.body); } catch (e) { return { error: (body && body.error) || 'no-galaxy' }; } var response = galaxyResult.response; // 'access-denied' isn't exactly right because it's possible that the galaxy // went down since our last request, but close enough. if (response.statusCode !== 200 || ! body || ! _.has(galaxyResult.setCookie, 'GALAXY_AUTH')) return { error: (body && body.error) || 'access-denied' }; return { token: galaxyResult.setCookie.GALAXY_AUTH, tokenId: body.tokenId }; }; // Prompt the user for a password, and then log in. Returns true if a // successful login was accomplished, else false. // // Options should include either 'email' or 'username', and may also // include: // - retry: if true, then if the user gets the password wrong, // reprompt. // - suppressErrorMessage: true if the function should not print an // error message to stderr if the login fails // - connection: an open connection to the accounts server. If not // provided, this function will open its own connection. var doInteractivePasswordLogin = function (options) { var loginData = {}; if (_.has(options, 'username')) loginData.username = options.username; else if (_.has(options, 'email')) loginData.email = options.email; else throw new Error("Need username or email"); var loginFailed = function () { if (! options.suppressErrorMessage) { Console.error("Login failed."); } }; var conn = options.connection || openAccountsConnection(); var maybeCloseConnection = function () { if (! options.connection) conn.close(); }; while (true) { loginData.password = Console.readLine({ echo: false, prompt: "Password: ", stream: process.stderr }); try { var result = conn.call('login', { session: auth.getSessionId(config.getAccountsDomain()), meteorAccountsLoginInfo: loginData, clientInfo: utils.getAgentInfo() }); } catch (err) { } if (result && result.token) { break; } else { loginFailed(); if (options.retry) { Console.error(); continue; } else { maybeCloseConnection(); return false; } } } if (result.session) { auth.setSessionId(config.getAccountsDomain(), result.session); } var data = readSessionData(); logOutAllSessions(data); var session = getSession(data, config.getAccountsDomain()); ensureSessionType(session, "meteor-account"); session.username = result.username; session.userId = result.id; session.token = result.token; session.tokenId = result.tokenId; writeSessionData(data); maybeCloseConnection(); return true; }; // options are the same as for doInteractivePasswordLogin, except without // username and email. exports.doUsernamePasswordLogin = function (options) { var username; do { username = Console.readLine({ prompt: "Username: ", stream: process.stderr }).trim(); } while (username.length === 0); return doInteractivePasswordLogin(_.extend({}, options, { username: username })); }; exports.doInteractivePasswordLogin = doInteractivePasswordLogin; exports.loginCommand = withAccountsConnection(function (options, connection) { config.printUniverseBanner(); var data = readSessionData(); var galaxy = options.galaxy; if (! galaxy && (! getSession(data, config.getAccountsDomain()).token || options.overwriteExistingToken)) { var loginOptions = {}; if (options.email) { loginOptions.email = Console.readLine({ prompt: "Email: ", stream: process.stderr }); } else { loginOptions.username = Console.readLine({ prompt: "Username: ", stream: process.stderr }); } loginOptions.connection = connection; if (! doInteractivePasswordLogin(loginOptions)) { return 1; } } // XXX Make the galaxy login not do a login if there is an existing token, just like MA if (galaxy) { var galaxyLoginResult = logInToGalaxy(galaxy); if (galaxyLoginResult.error) { // XXX add human readable error messages var failedLoginMsg = "\nLogin to ' + galaxy + ' failed. "; if (galaxyLoginResult.error === 'unauthorized') { Console.error( failedLoginMsg + 'You are not authorized for this galaxy.'); } else if (galaxyLoginResult.error === 'no_oauth_server') { Console.error( failedLoginMsg + 'The galaxy could not contact Meteor Accounts.'); } else if (galaxyLoginResult.error === 'no_identity') { Console.error( failedLoginMsg + 'Your login information could not be found.'); } else { Console.error(failedLoginMsg + 'Error: ' + galaxyLoginResult.error ); } return 1; } data = readSessionData(); // be careful to reread data file after RPC var session = getSession(data, galaxy); ensureSessionType(session, "galaxy"); session.token = galaxyLoginResult.token; session.tokenId = galaxyLoginResult.tokenId; writeSessionData(data); } tryRevokeOldTokens({ firstTry: true, connection: connection }); data = readSessionData(); Console.error(); Console.error("Logged in" + (galaxy ? " to " + galaxy : "") + (currentUsername(data) ? " as " + currentUsername(data) : "") + ".", "Thanks for being a Meteor developer!"); return 0; }); exports.logoutCommand = function (options) { config.printUniverseBanner(); var data = readSessionData(); var wasLoggedIn = !! loggedIn(data); logOutAllSessions(data); writeSessionData(data); tryRevokeOldTokens({ firstTry: true }); if (wasLoggedIn) Console.error("Logged out."); else // We called logOutAllSessions/writeSessionData anyway, out of an // abundance of caution. Console.error("Not logged in."); }; // If this is fully set up account (with a username and password), or // if not logged in, do nothing. If it is an account without a // username, contact the server and see if the user finished setting // it up, and if so grab and save the username. But contact the server // only once per run of the program. Options: // - noLogout: boolean. Set to true if you don't want this function to // log out the session if wehave an invalid credential (for example, // if a caller wants to do its own error handling for invalid // credentials). Defaults to false. var alreadyPolledForRegistration = false; exports.pollForRegistrationCompletion = function (options) { if (alreadyPolledForRegistration) return; alreadyPolledForRegistration = true; options = options || {}; var data = readSessionData(); var session = getSession(data, config.getAccountsDomain()); if (session.username || ! session.token) return; // We are logged in but we don't yet have a username. Ask the server // if a username was chosen since we last checked. var username = null; var fut = new Future(); var connection = loggedInAccountsConnection(session.token); if (! connection) { // Server says our credential isn't any good anymore! Get rid of // it. Note that, out of an abundance of caution, this also will // enqueue the credential for invalidation (on a future run, we // will try to explicitly revoke the credential ourselves). if (! options.noLogout) { logOutSession(session); writeSessionData(data); } return; } connection.call('getUsername', function (err, result) { if (fut.isResolved()) return; if (err) { // If anything went wrong, return null just as we would have if // we hadn't bothered to ask the server. fut['return'](null); return; } fut['return'](result); }); var timer = setTimeout(fiberHelpers.bindEnvironment(function () { if (! fut.isResolved()) { fut['return'](null); } }), 5000); username = fut.wait(); connection.close(); clearTimeout(timer); if (username) { writeMeteorAccountsUsername(username); } }; exports.registrationUrl = function () { var data = readSessionData(); var url = getSession(data, config.getAccountsDomain()).registrationUrl; return url; }; exports.whoAmICommand = function (options) { config.printUniverseBanner(); auth.pollForRegistrationCompletion(); var data = readSessionData(); if (! loggedIn(data)) { Console.error( "Not logged in. " + Console.command("'meteor login'") + " to log in."); return 1; } var username = currentUsername(data); if (username) { Console.rawInfo(username + "\n"); return 0; } var url = getSession(data, config.getAccountsDomain()).registrationUrl; if (url) { Console.error("You haven't chosen your username yet. To pick it, go here:"); Console.error(); Console.error(Console.url(url)); } else { // Won't happen in normal operation Console.error("You haven't chosen your username yet."); } return 1; }; // Prompt for an email address. If it doesn't belong to a user, create // a new deferred registration account and log in as it. If it does, // try to log the user into it. Returns true on success (user is now // logged in) or false on failure (user gave up, can't talk to // network..) exports.registerOrLogIn = withAccountsConnection(function (connection) { var result; // Get their email while (true) { var email = Console.readLine({ prompt: "Email: ", stream: process.stderr }); // Try to register try { var methodCaller = sessionMethodCaller( 'tryRegister', { connection: connection } ); result = methodCaller(email, utils.getAgentInfo()); break; } catch (err) { if (err.error === 400 && ! utils.validEmail(email)) { if (email.trim().length) { Console.error("Please double-check that address."); Console.error(); } } else { Console.error("\nCouldn't connect to server. " + "Check your internet connection."); return false; } } } var loginResult; if (! result.alreadyExisted) { var data = readSessionData(); logOutAllSessions(data); var session = getSession(data, config.getAccountsDomain()); ensureSessionType(session, "meteor-account"); session.token = result.token; session.tokenId = result.tokenId; session.userId = result.userId; session.registrationUrl = result.registrationUrl; writeSessionData(data); return true; } else if (result.alreadyExisted && result.sentRegistrationEmail) { Console.error(); Console.error( "You need to pick a password for your account so that you can log in.", "An email has been sent to you with the link."); Console.error(); var animationFrame = 0; var lastLinePrinted = ""; var timer = setInterval(function () { var spinner = ['-', '\\', '|', '/']; lastLinePrinted = "Waiting for you to register on the web... " + spinner[animationFrame]; Console.rawError(lastLinePrinted + "\r"); animationFrame = (animationFrame + 1) % spinner.length; }, 200); var stopSpinner = function () { Console.rawError(new Array(lastLinePrinted.length + 1).join(' ') + "\r"); clearInterval(timer); }; try { var waitForRegistrationResult = connection.call( 'waitForRegistration', email ); } catch (e) { stopSpinner(); if (e.errorType !== "Meteor.Error") throw e; Console.error( "When you've picked your password, run " + Console.command("'meteor login'") + " to log in."); return false; } stopSpinner(); Console.error("Username: " + waitForRegistrationResult.username); loginResult = doInteractivePasswordLogin({ username: waitForRegistrationResult.username, retry: true, connection: connection }); return loginResult; } else if (result.alreadyExisted && result.username) { Console.error("\nLogging in as " + Console.command(result.username) + "."); loginResult = doInteractivePasswordLogin({ username: result.username, retry: true, connection: connection }); return loginResult; } else { // Hmm, got an email we don't understand. Console.error( "\nThere was a problem. Please log in with " + Console.command("'meteor login'") + "."); return false; } }); // options: firstTime, leadingNewline // returns true if it printed something exports.maybePrintRegistrationLink = function (options) { options = options || {}; auth.pollForRegistrationCompletion(); var data = readSessionData(); var session = getSession(data, config.getAccountsDomain()); if (session.userId && ! session.username && session.registrationUrl) { if (options.leadingNewline) Console.error(); if (options.onlyAllowIfRegistered) { // A stronger message: we're going to not allow whatever they were trying // to do! Console.error( "You need to claim a username and set a password on your Meteor", "developer account to run this command. It takes about a minute at:", session.registrationUrl); Console.error(); } else if (! options.firstTime) { // If they've already been prompted to set a password then this // is more of a friendly reminder, so we word it slightly // differently than the first time they're being shown a // registration url. Console.error( "You should set a password on your Meteor developer account.", "It takes about a minute at:", session.registrationUrl); Console.error(); } else { Console.error( "You can set a password on your account or change your email", "address at:" + session.registrationUrl); Console.error(); } return true; } return false; }; exports.tryRevokeOldTokens = tryRevokeOldTokens; exports.getSessionId = function (domain, sessionData) { sessionData = sessionData || readSessionData(); return getSession(sessionData, domain).session; }; exports.setSessionId = function (domain, sessionId) { var data = readSessionData(); getSession(data, domain).session = sessionId; writeSessionData(data); }; exports.getSessionToken = function (domain) { return getSession(readSessionData(), domain).token; }; exports.isLoggedIn = function () { return loggedIn(readSessionData()); }; // Return the username of the currently logged in user, or false if // not logged in, or null if the logged in user doesn't have a // username. exports.loggedInUsername = function () { var data = readSessionData(); return loggedIn(data) ? currentUsername(data) : false; }; exports.getAccountsConfiguration = function (conn) { // Subscribe to the package server's service configurations so that we // can get the OAuth client ID to kick off the OAuth flow. var Package = getLoadedPackages(); var accountsConfiguration = null; // We avoid the overhead of creating a 'ddp-and-mongo' isopacket (or // always loading mongo whenever we load ddp) by just using the low-level // DDP client API here. conn.connection.registerStore('meteor_accounts_loginServiceConfiguration', { update: function (msg) { if (msg.msg === 'added' && msg.fields && msg.fields.service === 'meteor-developer') { // Note that this doesn't include the _id (which we'd have to parse), // but that's OK. accountsConfiguration = msg.fields; } } }); var serviceConfigurationsSub = conn.subscribeAndWait( 'meteor.loginServiceConfiguration'); if (! accountsConfiguration || ! accountsConfiguration.clientId) { throw new Error('no-accounts-configuration'); } return accountsConfiguration; }; // Given a ServiceConnection, log in with OAuth using Meteor developer // accounts. Assumes the user is already logged in to the developer // accounts server. exports.loginWithTokenOrOAuth = function (conn, accountsConfiguration, url, domain, sessionType) { var setUpOnReconnect = function () { conn.onReconnect = function () { conn.apply('login', [{ resume: auth.getSessionToken(domain) }], { wait: true }, function () { }); }; }; var clientId = accountsConfiguration.clientId; var loginResult; // Try to log in with an existing login token, if we have one. var existingToken = auth.getSessionToken(domain); if (existingToken) { try { loginResult = conn.apply('login', [{ resume: existingToken }], { wait: true }); } catch (err) { // If we get a Meteor.Error, then we swallow it and go on to // attempt an OAuth flow and get a new token. If it's not a // Meteor.Error, then we leave it to the caller to handle. if (err.errorType !== "Meteor.Error") { throw err; } } if (loginResult && loginResult.token && loginResult.id) { // Success! setUpOnReconnect(); return; } } // Either we didn't have an existing token, or it didn't work. Do an // OAuth flow to log in. var redirectUri = url + '/_oauth/meteor-developer?close'; loginResult = oauthFlow(conn, { clientId: clientId, redirectUri: redirectUri, domain: domain, sessionType: sessionType }); setUpOnReconnect(); }; exports.loggedInAccountsConnection = loggedInAccountsConnection; exports.withAccountsConnection = withAccountsConnection;