Files
meteor/tools/auth.js
2014-11-07 16:45:58 -08:00

1130 lines
35 KiB
JavaScript

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
process.stderr.write("warning: " +
(options.firstTry ?
"couldn't" : "still trying to") +
" confirm logout with " + domain +
"\n");
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://<galaxyName>:<DISCOVERY_PORT> 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) {
process.stderr.write("Login failed.\n");
}
};
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) {
process.stderr.write("\n");
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
process.stderr.write('\nLogin to ' + galaxy + ' failed. ');
if (galaxyLoginResult.error === 'unauthorized') {
process.stderr.write('You are not authorized for this galaxy.\n');
} else if (galaxyLoginResult.error === 'no_oauth_server') {
process.stderr.write('The galaxy could not ' +
'contact Meteor Accounts.\n');
} else if (galaxyLoginResult.error === 'no_identity') {
process.stderr.write('Your login information could not be found.\n');
} else {
process.stderr.write('Error: ' + galaxyLoginResult.error + '\n');
}
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();
process.stderr.write("\nLogged in" + (galaxy ? " to " + galaxy : "") +
(currentUsername(data) ?
" as " + currentUsername(data) : "") + ".\n" +
"Thanks for being a Meteor developer!\n");
return 0;
});
exports.logoutCommand = function (options) {
config.printUniverseBanner();
var data = readSessionData();
var wasLoggedIn = !! loggedIn(data);
logOutAllSessions(data);
writeSessionData(data);
tryRevokeOldTokens({ firstTry: true });
if (wasLoggedIn)
process.stderr.write("Logged out.\n");
else
// We called logOutAllSessions/writeSessionData anyway, out of an
// abundance of caution.
process.stderr.write("Not logged in.\n");
};
// 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)) {
process.stderr.write("Not logged in. 'meteor login' to log in.\n");
return 1;
}
var username = currentUsername(data);
if (username) {
process.stdout.write(username + "\n");
return 0;
}
var url = getSession(data, config.getAccountsDomain()).registrationUrl;
if (url) {
process.stderr.write(
"You haven't chosen your username yet. To pick it, go here:\n" +
"\n" +
url + "\n");
} else {
// Won't happen in normal operation
process.stderr.write("You haven't chosen your username yet.\n");
}
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)
process.stderr.write("Please double-check that address.\n\n");
} else {
process.stderr.write("\nCouldn't connect to server. " +
"Check your internet connection.\n");
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) {
process.stderr.write(
"\n" +
"You need to pick a password for your account so that you can log in.\n" +
"An email has been sent to you with the link.\n\n");
var animationFrame = 0;
var lastLinePrinted = "";
var timer = setInterval(function () {
var spinner = ['-', '\\', '|', '/'];
lastLinePrinted = "Waiting for you to register on the web... " +
spinner[animationFrame];
process.stderr.write(lastLinePrinted + "\r");
animationFrame = (animationFrame + 1) % spinner.length;
}, 200);
var stopSpinner = function () {
process.stderr.write(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;
process.stderr.write(
"When you've picked your password, run 'meteor login' to log in.\n")
return false;
}
stopSpinner();
process.stderr.write("Username: " +
waitForRegistrationResult.username + "\n");
loginResult = doInteractivePasswordLogin({
username: waitForRegistrationResult.username,
retry: true,
connection: connection
});
return loginResult;
} else if (result.alreadyExisted && result.username) {
process.stderr.write("\nLogging in as " + result.username + ".\n");
loginResult = doInteractivePasswordLogin({
username: result.username,
retry: true,
connection: connection
});
return loginResult;
} else {
// Hmm, got an email we don't understand.
process.stderr.write(
"\nThere was a problem. Please log in with 'meteor login'.\n");
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)
process.stderr.write("\n");
if (options.onlyAllowIfRegistered) {
// A stronger message: we're going to not allow whatever they were trying
// to do!
process.stderr.write(
"You need to claim a username and set a password on your Meteor developer\n" +
"account to run this command. It takes about a minute at:\n" +
" " + session.registrationUrl + "\n");
} 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.
process.stderr.write(
"You should set a password on your Meteor developer account. It takes\n" +
"about a minute at: " + session.registrationUrl + "\n\n");
} else {
process.stderr.write(
"You can set a password on your account or change your email address at:\n" +
session.registrationUrl + "\n\n");
}
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;