'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.UserManagerInstance = undefined; var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; let _startLoginServerAsync = (() => { var _ref = _asyncToGenerator(function* () { let dfd = new Deferred(); const server = _http.default.createServer(function (req, res) { if (req.method === 'POST' && req.url === '/callback') { let body = ''; req.on('data', function (data) { body += data; }); req.on('end', function () { dfd.resolve(_querystring.default.parse(body)); res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` Authenticated successfully! You can close this window. `); }); } else { res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(` `); } }); server.on('clientError', function (err, socket) { //eslint-disable-line socket.end('HTTP/1.1 400 Bad Request\r\n\r\n'); }); let connections = {}; server.on('connection', function (conn) { let key = conn.remoteAddress + ':' + conn.remotePort; connections[key] = conn; conn.on('close', function () { delete connections[key]; }); }); server.destroy = function (cb) { server.close(cb); for (let key in connections) { connections[key].destroy(); } }; const port = yield (0, (_freeportAsync || _load_freeportAsync()).default)(11000); try { server.listen(port, '127.0.0.1'); return { server, callbackURL: `http://127.0.0.1:${port}/callback`, getTokenInfoAsync: function () { return dfd.promise; } }; } catch (err) { throw err; } }); return function _startLoginServerAsync() { return _ref.apply(this, arguments); }; })(); var _lodash; function _load_lodash() { return _lodash = _interopRequireDefault(require('lodash')); } var _bluebird; function _load_bluebird() { return _bluebird = _interopRequireDefault(require('bluebird')); } var _freeportAsync; function _load_freeportAsync() { return _freeportAsync = _interopRequireDefault(require('freeport-async')); } var _http = _interopRequireDefault(require('http')); var _querystring = _interopRequireDefault(require('querystring')); var _opn; function _load_opn() { return _opn = _interopRequireDefault(require('opn')); } var _jsonwebtoken; function _load_jsonwebtoken() { return _jsonwebtoken = _interopRequireDefault(require('jsonwebtoken')); } var _ApiV; function _load_ApiV() { return _ApiV = _interopRequireDefault(require('./ApiV2')); } var _ApiV2; function _load_ApiV2() { return _ApiV2 = require('./ApiV2'); } var _Analytics; function _load_Analytics() { return _Analytics = _interopRequireWildcard(require('./Analytics')); } var _Config; function _load_Config() { return _Config = _interopRequireDefault(require('./Config')); } var _ErrorCode; function _load_ErrorCode() { return _ErrorCode = _interopRequireDefault(require('./ErrorCode')); } var _XDLError; function _load_XDLError() { return _XDLError = _interopRequireDefault(require('./XDLError')); } var _Logger; function _load_Logger() { return _Logger = _interopRequireDefault(require('./Logger')); } var _Intercom; function _load_Intercom() { return _Intercom = _interopRequireWildcard(require('./Intercom')); } var _UserSettings; function _load_UserSettings() { return _UserSettings = _interopRequireDefault(require('./UserSettings')); } var _Utils; function _load_Utils() { return _Utils = require('./Utils'); } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } const AUTH0_DOMAIN = 'exponent.auth0.com'; const AUTHENTICATION_SERVER_TIMEOUT = 1000 * 60 * 5; // 5 minutes class UserManagerInstance { constructor() { this.clientID = 'o0YygTgKhOTdoWj10Yl9nY2P0SMTw38Y'; this.loginServer = null; this.refreshSessionThreshold = 60 * 60; this._currentUser = null; this._getSessionLock = new (_Utils || _load_Utils()).Semaphore(); } // Default Client ID // 1 hour static getGlobalInstance() { if (!__globalInstance) { __globalInstance = new UserManagerInstance(); } return __globalInstance; } initialize(clientID) { if (clientID) { this.clientID = clientID; } this.loginServer = null; this._currentUser = null; this._getSessionLock = new (_Utils || _load_Utils()).Semaphore(); } /** * Logs in a user for a given login type. * * Valid login types are: * - "user-pass": Username and password authentication * - "facebook": Facebook authentication * - "google": Google authentication * - "github": Github authentication * * If the login type is "user-pass", we directly make the request to Auth0 * to login a user. * * If the login type is any of the social providers, we start a web server * that can act as the receiver of the OAuth callback from the authentication * process. The response we receive on that web server will be token data. */ loginAsync(loginType, loginArgs) { var _this = this; return _asyncToGenerator(function* () { let loginOptions; if (loginType === 'user-pass') { if (!loginArgs) { throw new Error(`The 'user-pass' login type requires a username and password.`); } loginOptions = { connection: 'Username-Password-Authentication', responseType: 'token', sso: false, username: loginArgs.username, password: loginArgs.password }; } else if (loginType === 'facebook') { loginOptions = { connection: 'facebook' }; } else if (loginType === 'google') { loginOptions = { connection: 'google-oauth2' }; } else if (loginType === 'github') { loginOptions = { connection: 'github' }; } else { throw new Error(`Invalid login type provided. Must be one of 'user-pass', 'facebook', 'google', or 'github'.`); } loginOptions = _extends({}, loginOptions, { scope: 'openid offline_access username nickname', // audience: 'https://exp.host', responseMode: 'form_post', responseType: 'token', device: 'xdl' }); let auth0Options = { clientID: _this.clientID }; if (loginType === 'user-pass') { try { const loginResp = yield _this._auth0LoginAsync(auth0Options, loginOptions); return yield _this._getProfileAsync({ currentConnection: loginOptions.connection, accessToken: loginResp.access_token, refreshToken: loginResp.refresh_token, idToken: loginResp.id_token, refreshTokenClientId: _this.clientID }); } catch (err) { throw err; } } // Doing a social login, so start a server const { server, callbackURL, getTokenInfoAsync } = yield _startLoginServerAsync(); // Kill server after 5 minutes if it hasn't already been closed const destroyServerTimer = setTimeout(function () { if (server.listening) { server.destroy(); } }, AUTHENTICATION_SERVER_TIMEOUT); auth0Options = { clientID: _this.clientID, callbackURL }; // Don't await -- we'll get response back through server // This will open a browser window _this._auth0LoginAsync(auth0Options, loginOptions); // Wait for token info to come back from server const tokenInfo = yield getTokenInfoAsync(); server.destroy(); clearTimeout(destroyServerTimer); const profile = yield _this._getProfileAsync({ currentConnection: loginOptions.connection, accessToken: tokenInfo.access_token, refreshToken: tokenInfo.refresh_token, idToken: tokenInfo.id_token, refreshTokenClientId: _this.clientID }); return profile; })(); } registerAsync(userData, user) { var _this2 = this; return _asyncToGenerator(function* () { if (!user) { user = yield _this2.getCurrentUserAsync(); } if (user && user.kind === 'user' && user.userMetadata && user.userMetadata.onboarded) { yield _this2.logoutAsync(); user = null; } let shouldUpdateUsernamePassword = true; if (user && user.kind === 'legacyUser') { // we're upgrading from an older client, // so login with username/pass if (userData.username && userData.password) { user = yield _this2.loginAsync('user-pass', { username: userData.username, password: userData.password }); } shouldUpdateUsernamePassword = false; } const currentUser = user; const shouldLinkAccount = currentUser && currentUser.currentConnection !== 'Username-Password-Authentication'; try { // Create or update the profile let registeredUser = yield _this2.createOrUpdateUserAsync(_extends({ connection: 'Username-Password-Authentication', // Always create/update username password email: userData.email, userMetadata: { onboarded: true, givenName: userData.givenName, familyName: userData.familyName } }, shouldUpdateUsernamePassword ? { username: userData.username } : {}, shouldLinkAccount ? { emailVerified: true } : {}, shouldUpdateUsernamePassword ? { password: userData.password } : {}, currentUser && shouldLinkAccount ? { forceCreate: true, linkedAccountId: currentUser.userId, linkedAccountConnection: currentUser.currentConnection } : {})); // if it's a new registration, or if they signed up with a social account, // we need to re-log them in with their username/pass. Otherwise, they're // already logged in. if (shouldLinkAccount || registeredUser && (!registeredUser.loginsCount || registeredUser.loginsCount && registeredUser.loginsCount < 1)) { // this is a new registration, log them in registeredUser = yield _this2.loginAsync('user-pass', { username: userData.username, password: userData.password }); } return registeredUser; } catch (e) { throw new (_XDLError || _load_XDLError()).default((_ErrorCode || _load_ErrorCode()).default.REGISTRATION_ERROR, 'Error registering user: ' + e.message); } })(); } /** * Ensure user is logged in and has a valid token. * * If there are any issues with the login, this method throws. */ ensureLoggedInAsync(options = { noTrackError: false }) { var _this3 = this; return _asyncToGenerator(function* () { if ((_Config || _load_Config()).default.offline) { return null; } const user = yield _this3.getCurrentUserAsync(); if (!user) { if (yield _this3.getLegacyUserData()) { throw new (_XDLError || _load_XDLError()).default((_ErrorCode || _load_ErrorCode()).default.LEGACY_ACCOUNT_ERROR, `We've updated our account system! Please login again by running \`exp login\`. Sorry for the inconvenience!`, { noTrack: options.noTrackError }); } throw new (_XDLError || _load_XDLError()).default((_ErrorCode || _load_ErrorCode()).default.NOT_LOGGED_IN, 'Not logged in', { noTrack: options.noTrackError }); } return user; })(); } /** * Get the current user based on the available token. * If there is no current token, returns null. */ getCurrentUserAsync() { var _this4 = this; return _asyncToGenerator(function* () { yield _this4._getSessionLock.acquire(); try { // If user is cached and token isn't expired // return the user if (_this4._currentUser && !_this4._isTokenExpired(_this4._currentUser.idToken)) { return _this4._currentUser; } if ((_Config || _load_Config()).default.offline) { return null; } // Not cached, check for token let { currentConnection, idToken, accessToken, refreshToken } = yield (_UserSettings || _load_UserSettings()).default.getAsync('auth', {}); // No tokens, no current user. Need to login if (!currentConnection || !idToken || !accessToken || !refreshToken) { return null; } try { return yield _this4._getProfileAsync({ currentConnection, accessToken, idToken, refreshToken }); } catch (e) { (_Logger || _load_Logger()).default.global.error(e); // This logs us out if theres a fatal error when getting the profile with // current access token // However, this also logs us out if there is a network error yield _this4.logoutAsync(); return null; } } finally { _this4._getSessionLock.release(); } })(); } /** * Get legacy user data from UserSettings. */ getLegacyUserData() { return _asyncToGenerator(function* () { const legacyUsername = yield (_UserSettings || _load_UserSettings()).default.getAsync('username', null); if (legacyUsername) { return { kind: 'legacyUser', username: legacyUsername, userMetadata: { legacy: true, needsPasswordMigration: true } }; } return null; })(); } /** * Create or update a user. */ createOrUpdateUserAsync(userData) { var _this5 = this; return _asyncToGenerator(function* () { let currentUser = _this5._currentUser; if (!currentUser) { // attempt to get the current user currentUser = yield _this5.getCurrentUserAsync(); } try { const api = (_ApiV || _load_ApiV()).default.clientForUser(_this5._currentUser); const { user: updatedUser } = yield api.postAsync('auth/createOrUpdateUser', { userData: _prepareAuth0Profile(userData) }); _this5._currentUser = _extends({}, _this5._currentUser || {}, _parseAuth0Profile(updatedUser)); return _extends({ kind: 'user' }, _this5._currentUser); } catch (e) { const err = e; if (err.code === 'AUTHENTICATION_ERROR') { throw new Error(err.details.message); } throw e; } })(); } /** * Logout */ logoutAsync() { var _this6 = this; return _asyncToGenerator(function* () { if (_this6._currentUser) { (_Analytics || _load_Analytics()).logEvent('Logout', { username: _this6._currentUser.username }); } _this6._currentUser = null; // Delete saved JWT yield (_UserSettings || _load_UserSettings()).default.deleteKeyAsync('auth'); // Delete legacy auth yield (_UserSettings || _load_UserSettings()).default.deleteKeyAsync('username'); // Logout of Intercom (_Intercom || _load_Intercom()).update(null); })(); } /** * Forgot Password */ forgotPasswordAsync(usernameOrEmail) { var _this7 = this; return _asyncToGenerator(function* () { return yield _this7._auth0ForgotPasswordAsync(usernameOrEmail); })(); } /** * Get profile given token data. Errors if token is not valid or if no * user profile is returned. * * This method is called by all public authentication methods of `UserManager` * except `logoutAsync`. Therefore, we use this method as a way to: * - update the UserSettings store with the current token and user id * - update UserManager._currentUser * - Fire login analytics events * - Update the currently assigned Intercom user * * Also updates UserManager._currentUser. * * @private */ _getProfileAsync({ currentConnection, accessToken, idToken, refreshToken, refreshTokenClientId }) { var _this8 = this; return _asyncToGenerator(function* () { // Attempt to grab profile from Auth0. // If token is expired / getting the profile fails, use refresh token to let user; try { const dtoken = (_jsonwebtoken || _load_jsonwebtoken()).default.decode(idToken, { complete: true }); const { aud } = dtoken.payload; // If it's not a new login, refreshTokenClientId won't be set in the arguments. // In this case, try to get the currentRefreshTokenClientId from UserSettings, // otherwise, default back to the audience of the current id_token if (!refreshTokenClientId) { const { refreshTokenClientId: currentRefreshTokenClientId } = yield (_UserSettings || _load_UserSettings()).default.getAsync('auth', {}); if (!currentRefreshTokenClientId) { refreshTokenClientId = aud; // set it to the "aud" property of the existing token } else { refreshTokenClientId = currentRefreshTokenClientId; } } // TODO(@skevy): remove if (process.env.NODE_ENV !== 'production') { (_Logger || _load_Logger()).default.global.debug('REFRESH_TOKEN_CLIENT_ID', refreshTokenClientId); } // TODO(@skevy): remove if (process.env.NODE_ENV !== 'production') { (_Logger || _load_Logger()).default.global.debug('DECODED TOKEN', dtoken); } if (_this8._isTokenExpired(idToken)) { // if there's less than the refresh session threshold left on the token, refresh it // TODO(@skevy): remove if (process.env.NODE_ENV !== 'production') { (_Logger || _load_Logger()).default.global.debug('REFRESHING ID TOKEN'); } // TODO(@skevy): remove if (process.env.NODE_ENV !== 'production') { (_Logger || _load_Logger()).default.global.debug('REFRESH TOKEN', refreshToken); } const delegationResult = yield _this8._auth0RefreshToken(refreshTokenClientId, // client id that's associated with the refresh token refreshToken // refresh token to use ); // TODO(@skevy): remove if (process.env.NODE_ENV !== 'production') { (_Logger || _load_Logger()).default.global.debug('SUCCESSFULLY GOT NEW ID TOKEN'); } idToken = delegationResult.id_token; // TODO(@skevy): remove if (process.env.NODE_ENV !== 'production') { (_Logger || _load_Logger()).default.global.debug('NEW ID TOKEN', idToken); } } // TODO(@skevy): remove if (process.env.NODE_ENV !== 'production') { (_Logger || _load_Logger()).default.global.debug('ID TOKEN FOR PROFILE', idToken); } user = yield _this8._auth0GetProfileAsync(idToken); // TODO(@skevy): remove if (process.env.NODE_ENV !== 'production') { (_Logger || _load_Logger()).default.global.debug('USER DATA', user); } if (!user) { throw new Error('No user profile associated with this token'); } } catch (e) { throw e; } if (!user) { throw new Error('Unable to fetch user.'); } user = _extends({}, _parseAuth0Profile(user), { kind: 'user', currentConnection, accessToken, idToken, refreshToken }); yield (_UserSettings || _load_UserSettings()).default.mergeAsync({ auth: _extends({ userId: user.userId, username: user.username, currentConnection, accessToken, idToken, refreshToken }, refreshTokenClientId ? { refreshTokenClientId } : {}) }); yield (_UserSettings || _load_UserSettings()).default.deleteKeyAsync('username'); // If no currentUser, or currentUser.id differs from profiles // user id, that means we have a new login if ((!_this8._currentUser || _this8._currentUser.userId !== user.userId) && user.username && user.username !== '') { (_Analytics || _load_Analytics()).logEvent('Login', { userId: user.userId, currentConnection: user.currentConnection, username: user.username }); (_Analytics || _load_Analytics()).setUserProperties(user.username, { userId: user.userId, currentConnection: user.currentConnection, username: user.username }); if (user.intercomUserHash) { (_Intercom || _load_Intercom()).update(user); } } else { (_Intercom || _load_Intercom()).update(null); } _this8._currentUser = user; return user; })(); } _isTokenExpired(idToken) { const dtoken = (_jsonwebtoken || _load_jsonwebtoken()).default.decode(idToken, { complete: true }); const { exp } = dtoken.payload; // TODO(@skevy): remove if (process.env.NODE_ENV !== 'production') { (_Logger || _load_Logger()).default.global.debug('TOKEN EXPIRATION', exp); } // TODO(@skevy): remove if (process.env.NODE_ENV !== 'production') { (_Logger || _load_Logger()).default.global.debug('TOKEN TIME LEFT', exp - Date.now() / 1000); } return exp - Date.now() / 1000 <= this.refreshSessionThreshold; } _auth0LoginAsync(auth0Options, loginOptions) { return _asyncToGenerator(function* () { if (typeof window !== 'undefined' && window) { const Auth0JS = _auth0JSInstanceWithOptions(auth0Options); const resp = yield Auth0JS.loginAsync(loginOptions); return { access_token: resp.accessToken, id_token: resp.idToken, refresh_token: resp.refreshToken }; } const Auth0Node = _nodeAuth0InstanceWithOptions(auth0Options); if (loginOptions.connection === 'Username-Password-Authentication') { try { return yield Auth0Node.oauth.signIn(loginOptions); } catch (e) { throw _formatAuth0NodeError(e); } } else { // social (0, (_opn || _load_opn()).default)(_buildAuth0SocialLoginUrl(auth0Options, loginOptions), { wait: false }); return {}; } })(); } _auth0GetProfileAsync(idToken) { var _this9 = this; return _asyncToGenerator(function* () { if (typeof window !== 'undefined' && window) { const Auth0JS = _auth0JSInstanceWithOptions({ clientID: _this9.clientID }); return yield Auth0JS.getProfileAsync(idToken); } const Auth0Node = _nodeAuth0InstanceWithOptions({ clientID: _this9.clientID }); const profile = yield Auth0Node.tokens.getInfo(idToken); return profile; })(); } _auth0RefreshToken(clientId, refreshToken) { var _this10 = this; return _asyncToGenerator(function* () { const delegationTokenOptions = { refresh_token: refreshToken, api_type: 'app', scope: 'openid offline_access nickname username', target: _this10.clientID, client_id: clientId }; if (typeof window !== 'undefined' && window) { const Auth0JS = _auth0JSInstanceWithOptions({ clientID: clientId }); return yield Auth0JS.getDelegationTokenAsync(_extends({}, delegationTokenOptions)); } const Auth0Node = _nodeAuth0InstanceWithOptions({ clientID: _this10.clientID }); const delegationResult = yield Auth0Node.tokens.getDelegationToken(_extends({ grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer' }, delegationTokenOptions)); return delegationResult; })(); } _auth0ForgotPasswordAsync(usernameOrEmail) { var _this11 = this; return _asyncToGenerator(function* () { if (typeof window !== 'undefined' && window) { const Auth0JS = _auth0JSInstanceWithOptions({ clientID: _this11.clientID }); return yield Auth0JS.changePasswordAsync({ connection: 'Username-Password-Authentication', email: usernameOrEmail }); } const Auth0Node = _nodeAuth0InstanceWithOptions({ clientID: _this11.clientID }); return yield Auth0Node.database.changePassword({ connection: 'Username-Password-Authentication', email: usernameOrEmail }); })(); } } exports.UserManagerInstance = UserManagerInstance; let __globalInstance; exports.default = UserManagerInstance.getGlobalInstance(); /** Private Methods **/ function _formatAuth0NodeError(e) { // TODO: Fix the Auth0 js library to throw better error messages when the network fails. // Auth0 returns an error object whenver Auth0 fails to make an API request. // These error messages are usually well-formed when you have an invalid login or too many attempts, // but when the network is down it does not give any meaningful messages. // Network failures log the user out in _getCurrentUserAsync() when it uses Auth0. const errData = e.message; switch (errData.error) { case 'invalid_user_password': return new (_XDLError || _load_XDLError()).default((_ErrorCode || _load_ErrorCode()).default.INVALID_USERNAME_PASSWORD, 'Invalid username or password'); case 'too_many_attempts': return new (_XDLError || _load_XDLError()).default((_ErrorCode || _load_ErrorCode()).default.TOO_MANY_ATTEMPTS, errData.error_description); default: return new Error(errData.error_description); } return e; } function _buildAuth0SocialLoginUrl(auth0Options, loginOptions) { const qsData = { scope: 'openid offline_access username nickname', response_type: loginOptions.responseType, response_mode: loginOptions.responseMode, connection: loginOptions.connection, device: 'xdl', client_id: auth0Options.clientID, redirect_uri: auth0Options.callbackURL }; const queryString = _querystring.default.stringify(qsData); return `https://${AUTH0_DOMAIN}/authorize?${queryString}`; } function _auth0JSInstanceWithOptions(options = {}) { const Auth0 = require('auth0-js'); let auth0Options = _extends({ domain: AUTH0_DOMAIN, responseType: 'token' }, options); const Auth0Instance = (_bluebird || _load_bluebird()).default.promisifyAll(new Auth0(auth0Options)); return Auth0Instance; } function _nodeAuth0InstanceWithOptions(options = {}) { let auth0Options = _extends({ domain: AUTH0_DOMAIN, clientId: options.clientID || options.clientId }, options); let Auth0Instance; if (auth0Options.management === true) { auth0Options = (_lodash || _load_lodash()).default.omit(auth0Options, 'management'); const ManagementClient = require('auth0').ManagementClient; Auth0Instance = new ManagementClient(auth0Options); } else { const AuthenticationClient = require('auth0').AuthenticationClient; Auth0Instance = new AuthenticationClient(auth0Options); } return Auth0Instance; } function _parseAuth0Profile(rawProfile) { if (!rawProfile || typeof rawProfile !== 'object') { return rawProfile; } return Object.keys(rawProfile).reduce((p, key) => { p[(_lodash || _load_lodash()).default.camelCase(key)] = _parseAuth0Profile(rawProfile[key]); return p; }, {}); } function _prepareAuth0Profile(niceProfile) { if (typeof niceProfile !== 'object') { return niceProfile; } return Object.keys(niceProfile).reduce((p, key) => { p[(_lodash || _load_lodash()).default.snakeCase(key)] = _prepareAuth0Profile(niceProfile[key]); return p; }, {}); } class Deferred { constructor() { this.promise = new Promise((resolve, reject) => { this.reject = reject; this.resolve = resolve; }); } } //# sourceMappingURL=__sourcemaps__/User.js.map