tools/meteor-services/auth.js
var _ = require('underscore');
var utils = require('../utils/utils.js');
var files = require('../fs/files');
var config = require('./config.js');
var httpHelpers = require('../utils/http-helpers.js');
var fiberHelpers = require('../utils/fiber-helpers.js');
var querystring = require('querystring');
var url = require('url');
var Console = require('../console/console.js').Console;
var auth = exports;
function loadDDP() {
return require("../tool-env/isopackets.js")
.loadIsopackage("ddp-client")
.DDP;
}
// Opens and returns a DDP connection to the accounts server. Remember
// to close it when you're done with it!
var openAccountsConnection = function () {
return loadDDP().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 (...args) {
var self = this;
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 = loadDDP().connect(
config.getAuthDDPUrl()
);
return new Promise(function (resolve, reject) {
connection.apply(
'login',
[{ resume: token }],
{ wait: true },
function (err) {
err ? reject(err) : resolve(connection);
}
);
}).catch(function (err) {
connection.close();
if (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 err;
}).await();
};
// 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 (...args) {
args.push({
session: auth.getSessionId(config.getAccountsDomain()) || null
});
var timer;
var conn = options.connection || openAccountsConnection();
function cleanUp() {
timer && clearTimeout(timer);
options.connection || conn.close();
}
return new Promise(function (resolve, reject) {
conn.apply(methodName, args, function (err, res) {
err ? reject(err) : resolve(res);
});
if (options.timeout !== undefined) {
timer = setTimeout(function () {
reject(new Error('Method call timed out'));
}, options.timeout);
}
}).then(function (result) {
cleanUp();
if (result) {
if (result.session) {
// The bindEnvironment call above ensures the file IO operations
// that happen in auth.setSessionId take place in a Fiber.
auth.setSessionId(config.getAccountsDomain(), result.session);
}
result = result.result;
}
return result;
}, function (err) {
cleanUp();
throw err;
}).await();
};
};
var readSessionData = function () {
var sessionPath = config.getSessionFilePath();
if (! files.exists(sessionPath)) {
return {};
}
return JSON.parse(files.readFile(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 =
files.pathJoin(files.pathDirname(sessionPath), '.meteorsession.' +
Math.floor(Math.random() * 999999));
try {
var fd = files.open(tempPath, 'wx', 0o600);
} catch (e) {
continue;
}
try {
// Write `data` to the file.
var buf = Buffer.from(JSON.stringify(data, undefined, 2), 'utf8');
files.write(fd, buf, 0, buf.length, 0);
} finally {
files.close(fd);
}
// Atomically remove the old file (if any) and replace it with
// the temporary file we just created.
files.rename(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
// We previously used:
// - "galaxy": a login to a legacy Galaxy prototype server
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).
Object.keys(session).forEach(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);
};
// 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 = Object.assign({
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") {
// These are tokens from a legacy Galaxy prototype, which cannot be
// revoked (because the prototype no longer exists), but we can at least
// remove them from the file.
removePendingRevoke(domain, tokenIds);
} else {
// don't know how to revoke tokens of this type
logoutFailWarning(domain);
return;
}
});
};
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');
}
};
// 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");
}
if (_.has(options, 'password')) {
loginData.password = options.password;
}
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) {
if (! _.has(loginData, 'password')) {
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) {
delete loginData.password;
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(Object.assign({}, options, {
username: username
}));
};
exports.doInteractivePasswordLogin = doInteractivePasswordLogin;
exports.loginCommand = withAccountsConnection(function (options,
connection) {
var data = readSessionData();
if (! 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;
}
}
tryRevokeOldTokens({ firstTry: true, connection: connection });
data = readSessionData();
Console.error();
Console.error("Logged in" +
(currentUsername(data) ? " as " + currentUsername(data) : "") +
". Thanks for being a Meteor developer!");
return 0;
});
exports.logoutCommand = function (options) {
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 connection = loggedInAccountsConnection(session.token);
var timer;
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;
}
new Promise(function (resolve) {
connection.call('getUsername', function (err, username) {
// If anything went wrong, return null just as we would have if we
// hadn't bothered to ask the server.
resolve(err ? null : username);
});
timer = setTimeout(function () {
resolve(null);
}, 5000);
// Intentionally calling bindEnvironment on the .then callback rather
// than the function that calls resolve.
}).then(fiberHelpers.bindEnvironment(function (username) {
connection.close();
clearTimeout(timer);
if (username) {
writeMeteorAccountsUsername(username);
}
// We don't actually care about the result, just that the side-effects
// of writeMeteorAccountsUsername happen.
})).await();
};
exports.registrationUrl = function () {
var data = readSessionData();
var url = getSession(data, config.getAccountsDomain()).registrationUrl;
return url;
};
exports.whoAmICommand = function (options) {
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 + Console.CARRIAGE_RETURN);
animationFrame = (animationFrame + 1) % spinner.length;
}, 200);
var stopSpinner = function () {
Console.rawError(new Array(lastLinePrinted.length + 1).join(' ') +
Console.CARRIAGE_RETURN);
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 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';
// Duplicate code from packages/oauth/oauth_common.js. In Meteor 0.9.1, we
// switched to a new URL style for Oauth that no longer has the "?close"
// parameter at the end. However, we need all of our backend services to be
// compatible with old Meteor tools which were written before 0.9.1. These old
// meteor tools only know how to deal with oauth URLs that have the "?close"
// query parameter, so our services (packages.meteor.com, etc) have to use the
// old-style URL. This means that all new Meteor tools also need to use the
// old-style URL to be compatible with the new servers which are backwards-
// compatible with the old tool.
if (! accountsConfiguration.loginStyle) {
redirectUri = redirectUri + "?close";
}
loginResult = oauthFlow(conn, {
clientId: clientId,
redirectUri: redirectUri,
domain: domain,
sessionType: sessionType
});
setUpOnReconnect();
};
exports.loggedInAccountsConnection = loggedInAccountsConnection;
exports.withAccountsConnection = withAccountsConnection;