diff --git a/src/background.js b/src/background.js index ae98520c81..40b40e82c8 100644 --- a/src/background.js +++ b/src/background.js @@ -34,7 +34,7 @@ var loadMenuRan = false, autofillTimeout = null; chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) { - if (msg.command === 'loggedOut' || msg.command === 'loggedIn' || msg.command === 'unlocked' || msg.command === 'locked') { + if (msg.command === 'loggedIn' || msg.command === 'unlocked' || msg.command === 'locked') { if (loadMenuRan) { return; } @@ -43,6 +43,17 @@ chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) { setIcon(); refreshBadgeAndMenu(); } + else if (msg.command === 'logout') { + logout(msg.expired, function () { + if (loadMenuRan) { + return; + } + loadMenuRan = true; + + setIcon(); + refreshBadgeAndMenu(); + }); + } else if (msg.command === 'syncCompleted' && msg.successfully) { if (loadMenuRan) { return; @@ -584,6 +595,30 @@ function loadContextMenuOptions(title, idSuffix, login) { } } +// TODO: Fix callback hell by moving to promises +function logout(expired, callback) { + userService.getUserId(function (userId) { + syncService.setLastSync(new Date(0), function () { + settingsService.clear(function () { + tokenService.clearToken(function () { + cryptoService.clearKey(function () { + cryptoService.clearKeyHash(function () { + userService.clearUserIdAndEmail(function () { + loginService.clear(userId, function () { + folderService.clear(userId, function () { + chrome.runtime.sendMessage({ command: 'doneLoggingOut', expired: expired }); + callback(); + }); + }); + }); + }); + }); + }); + }); + }); + }); +} + function copyToClipboard(text) { if (window.clipboardData && window.clipboardData.setData) { // IE specific code path to prevent textarea being shown while dialog is visible. diff --git a/src/popup/app/config.js b/src/popup/app/config.js index 2d912fcb46..624f44a965 100644 --- a/src/popup/app/config.js +++ b/src/popup/app/config.js @@ -190,7 +190,7 @@ params: { animation: null } }); }) - .run(function ($rootScope, userService, authService, cryptoService, tokenService, $state, constantsService, stateService) { + .run(function ($rootScope, userService, cryptoService, tokenService, $state, constantsService, stateService) { $rootScope.$on('$stateChangeStart', function (event, toState, toParams) { if ($state.current.name.indexOf('tabs.') > -1 && toState.name.indexOf('tabs.') > -1) { stateService.purgeState(); @@ -220,9 +220,7 @@ if (!isAuthenticated || tokenService.isTokenExpired()) { event.preventDefault(); - authService.logOut(function () { - $state.go('home'); - }); + chrome.runtime.sendMessage({ command: 'logout' }); } }); }); diff --git a/src/popup/app/global/mainController.js b/src/popup/app/global/mainController.js index 1c656438ee..8c96dcf9b7 100644 --- a/src/popup/app/global/mainController.js +++ b/src/popup/app/global/mainController.js @@ -1,7 +1,7 @@ angular .module('bit.global') - .controller('mainController', function ($scope, $state, authService, toastr, i18nService) { + .controller('mainController', function ($scope, $state, authService, toastr, i18nService, $analytics) { var self = this; self.currentYear = new Date().getFullYear(); self.animation = ''; @@ -23,9 +23,12 @@ angular else if (msg.command === 'syncStarted') { $scope.$broadcast('syncStarted'); } - else if (msg.command === 'logout') { + else if (msg.command === 'doneLoggingOut') { authService.logOut(function () { - toastr.warning(i18nService.loginExpired, i18nService.loggedOut); + $analytics.eventTrack('Logged Out'); + if (msg.expired) { + toastr.warning(i18nService.loginExpired, i18nService.loggedOut); + } $state.go('home'); }); } diff --git a/src/popup/app/lock/lockController.js b/src/popup/app/lock/lockController.js index 032ffe0f6e..ad3e2920ce 100644 --- a/src/popup/app/lock/lockController.js +++ b/src/popup/app/lock/lockController.js @@ -1,7 +1,7 @@ angular .module('bit.lock') - .controller('lockController', function ($scope, $state, $analytics, i18nService, authService, cryptoService, toastr, + .controller('lockController', function ($scope, $state, $analytics, i18nService, cryptoService, toastr, userService, SweetAlert) { $scope.i18n = i18nService; $('#master-password').focus(); @@ -15,10 +15,7 @@ cancelButtonText: i18nService.cancel }, function (confirmed) { if (confirmed) { - authService.logOut(function () { - $analytics.eventTrack('Logged Out'); - $state.go('home'); - }); + chrome.runtime.sendMessage({ command: 'logout' }); } }); }; diff --git a/src/popup/app/services/authService.js b/src/popup/app/services/authService.js index 9f018596a1..d9bc727f9c 100644 --- a/src/popup/app/services/authService.js +++ b/src/popup/app/services/authService.js @@ -39,30 +39,10 @@ return deferred.promise; }; - // TODO: Fix callback hell by moving to promises _service.logOut = function (callback) { - userService.getUserId(function (userId) { - syncService.setLastSync(new Date(0), function () { - settingsService.clear(function () { - tokenService.clearToken(function () { - cryptoService.clearKey(function () { - cryptoService.clearKeyHash(function () { - userService.clearUserIdAndEmail(function () { - loginService.clear(userId, function () { - folderService.clear(userId, function () { - $rootScope.vaultLogins = null; - $rootScope.vaultFolders = null; - chrome.runtime.sendMessage({ command: 'loggedOut' }); - callback(); - }); - }); - }); - }); - }); - }); - }); - }); - }); + $rootScope.vaultLogins = null; + $rootScope.vaultFolders = null; + callback(); }; return _service; diff --git a/src/popup/app/settings/settingsController.js b/src/popup/app/settings/settingsController.js index 1f307b4fbe..3277e7ba27 100644 --- a/src/popup/app/settings/settingsController.js +++ b/src/popup/app/settings/settingsController.js @@ -1,7 +1,7 @@ angular .module('bit.settings') - .controller('settingsController', function ($scope, authService, $state, SweetAlert, utilsService, $analytics, + .controller('settingsController', function ($scope, $state, SweetAlert, utilsService, $analytics, i18nService, constantsService, cryptoService) { utilsService.initListSectionItemListeners($(document), angular); $scope.lockOption = ''; @@ -38,10 +38,7 @@ }, function (confirmed) { if (confirmed) { cryptoService.toggleKey(function () { }); - authService.logOut(function () { - $analytics.eventTrack('Logged Out'); - $state.go('home'); - }); + chrome.runtime.sendMessage({ command: 'logout' }); } }); } @@ -58,10 +55,7 @@ cancelButtonText: i18nService.cancel }, function (confirmed) { if (confirmed) { - authService.logOut(function () { - $analytics.eventTrack('Logged Out'); - $state.go('home'); - }); + chrome.runtime.sendMessage({ command: 'logout' }); } }); }; diff --git a/src/services/apiService.js b/src/services/apiService.js index 8bdfaa7bdb..c67ff0d950 100644 --- a/src/services/apiService.js +++ b/src/services/apiService.js @@ -1,5 +1,5 @@ function ApiService(tokenService) { - this.baseUrl = 'http://localhost:4000'; + this.baseUrl = 'https://api.bitwarden.com'; this.tokenService = tokenService; initApiService(); @@ -36,7 +36,7 @@ function initApiService() { ApiService.prototype.getAccountRevisionDate = function (success, error) { var self = this; - this.tokenService.getToken(function (token) { + handleTokenState(self).then(function (token) { $.ajax({ type: 'GET', url: self.baseUrl + '/accounts/revision-date?access_token2=' + token, @@ -45,15 +45,17 @@ function initApiService() { success(response); }, error: function (jqXHR, textStatus, errorThrown) { - handleError(error, jqXHR, textStatus, errorThrown); + handleError(error, jqXHR); } }); + }, function (jqXHR) { + handleError(error, jqXHR, true); }); }; ApiService.prototype.getProfile = function (success, error) { var self = this; - this.tokenService.getToken(function (token) { + handleTokenState(self).then(function (token) { $.ajax({ type: 'GET', url: self.baseUrl + '/accounts/profile?access_token2=' + token, @@ -62,9 +64,11 @@ function initApiService() { success(new ProfileResponse(response)); }, error: function (jqXHR, textStatus, errorThrown) { - handleError(error, jqXHR, textStatus, errorThrown); + handleError(error, jqXHR); } }); + }, function (jqXHR) { + handleError(error, jqXHR, true); }); }; @@ -80,7 +84,7 @@ function initApiService() { success(); }, error: function (jqXHR, textStatus, errorThrown) { - handleError(error, jqXHR, textStatus, errorThrown); + handleError(error, jqXHR); } }); }; @@ -97,7 +101,7 @@ function initApiService() { success(); }, error: function (jqXHR, textStatus, errorThrown) { - handleError(error, jqXHR, textStatus, errorThrown); + handleError(error, jqXHR); } }); }; @@ -106,7 +110,7 @@ function initApiService() { ApiService.prototype.getIncludedDomains = function (success, error) { var self = this; - this.tokenService.getToken(function (token) { + handleTokenState(self).then(function (token) { $.ajax({ type: 'GET', url: self.baseUrl + '/settings/domains?excluded=false&access_token2=' + token, @@ -115,9 +119,11 @@ function initApiService() { success(new DomainsResponse(response)); }, error: function (jqXHR, textStatus, errorThrown) { - handleError(error, jqXHR, textStatus, errorThrown); + handleError(error, jqXHR); } }); + }, function (jqXHR) { + handleError(error, jqXHR, true); }); }; @@ -125,7 +131,7 @@ function initApiService() { ApiService.prototype.getLogin = function (id, success, error) { var self = this; - this.tokenService.getToken(function (token) { + handleTokenState(self).then(function (token) { $.ajax({ type: 'GET', url: self.baseUrl + '/sites/' + id + '?access_token2=' + token, @@ -134,15 +140,17 @@ function initApiService() { success(new LoginResponse(response)); }, error: function (jqXHR, textStatus, errorThrown) { - handleError(error, jqXHR, textStatus, errorThrown); + handleError(error, jqXHR); } }); + }, function (jqXHR) { + handleError(error, jqXHR, true); }); }; ApiService.prototype.postLogin = function (loginRequest, success, error) { var self = this; - this.tokenService.getToken(function (token) { + handleTokenState(self).then(function (token) { $.ajax({ type: 'POST', url: self.baseUrl + '/sites?access_token2=' + token, @@ -153,15 +161,17 @@ function initApiService() { success(new LoginResponse(response)); }, error: function (jqXHR, textStatus, errorThrown) { - handleError(error, jqXHR, textStatus, errorThrown); + handleError(error, jqXHR); } }); + }, function (jqXHR) { + handleError(error, jqXHR, true); }); }; ApiService.prototype.putLogin = function (id, loginRequest, success, error) { var self = this; - this.tokenService.getToken(function (token) { + handleTokenState(self).then(function (token) { $.ajax({ type: 'POST', url: self.baseUrl + '/sites/' + id + '?access_token2=' + token, @@ -172,9 +182,11 @@ function initApiService() { success(new LoginResponse(response)); }, error: function (jqXHR, textStatus, errorThrown) { - handleError(error, jqXHR, textStatus, errorThrown); + handleError(error, jqXHR); } }); + }, function (jqXHR) { + handleError(error, jqXHR, true); }); }; @@ -182,7 +194,7 @@ function initApiService() { ApiService.prototype.getFolder = function (id, success, error) { var self = this; - this.tokenService.getToken(function (token) { + handleTokenState(self).then(function (token) { $.ajax({ type: 'GET', url: self.baseUrl + '/folders/' + id + '?access_token2=' + token, @@ -191,15 +203,17 @@ function initApiService() { success(new FolderResponse(response)); }, error: function (jqXHR, textStatus, errorThrown) { - handleError(error, jqXHR, textStatus, errorThrown); + handleError(error, jqXHR); } }); - }); + }, function (jqXHR) { + handleError(error, jqXHR, true); + }); }; ApiService.prototype.postFolder = function (folderRequest, success, error) { var self = this; - this.tokenService.getToken(function (token) { + handleTokenState(self).then(function (token) { $.ajax({ type: 'POST', url: self.baseUrl + '/folders?access_token2=' + token, @@ -210,15 +224,17 @@ function initApiService() { success(new FolderResponse(response)); }, error: function (jqXHR, textStatus, errorThrown) { - handleError(error, jqXHR, textStatus, errorThrown); + handleError(error, jqXHR); } }); + }, function (jqXHR) { + handleError(error, jqXHR, true); }); }; ApiService.prototype.putFolder = function (id, folderRequest, success, error) { var self = this; - this.tokenService.getToken(function (token) { + handleTokenState(self).then(function (token) { $.ajax({ type: 'POST', url: self.baseUrl + '/folders/' + id + '?access_token2=' + token, @@ -229,9 +245,11 @@ function initApiService() { success(new FolderResponse(response)); }, error: function (jqXHR, textStatus, errorThrown) { - handleError(error, jqXHR, textStatus, errorThrown); + handleError(error, jqXHR); } }); + }, function (jqXHR) { + handleError(error, jqXHR, true); }); }; @@ -239,7 +257,7 @@ function initApiService() { ApiService.prototype.getCipher = function (id, success, error) { var self = this; - this.tokenService.getToken(function (token) { + handleTokenState(self).then(function (token) { $.ajax({ type: 'GET', url: self.baseUrl + '/ciphers/' + id + '?access_token2=' + token, @@ -248,15 +266,17 @@ function initApiService() { success(new CipherResponse(response)); }, error: function (jqXHR, textStatus, errorThrown) { - handleError(error, jqXHR, textStatus, errorThrown); + handleError(error, jqXHR); } }); + }, function (jqXHR) { + handleError(error, jqXHR, true); }); }; ApiService.prototype.getCiphers = function (success, error) { var self = this; - this.tokenService.getToken(function (token) { + handleTokenState(self).then(function (token) { $.ajax({ type: 'GET', url: self.baseUrl + '/ciphers?access_token2=' + token, @@ -270,15 +290,17 @@ function initApiService() { success(new ListResponse(data)); }, error: function (jqXHR, textStatus, errorThrown) { - handleError(error, jqXHR, textStatus, errorThrown); + handleError(error, jqXHR); } }); + }, function (jqXHR) { + handleError(error, jqXHR, true); }); }; ApiService.prototype.deleteCipher = function (id, success, error) { var self = this; - this.tokenService.getToken(function (token) { + handleTokenState(self).then(function (token) { $.ajax({ type: 'POST', url: self.baseUrl + '/ciphers/' + id + '/delete?access_token2=' + token, @@ -287,20 +309,57 @@ function initApiService() { success(); }, error: function (jqXHR, textStatus, errorThrown) { - handleError(error, jqXHR, textStatus, errorThrown); + handleError(error, jqXHR); } }); + }, function (jqXHR) { + handleError(error, jqXHR, true); }); }; // Helpers - function handleError(errorCallback, jqXHR, textStatus, errorThrown) { - if (jqXHR.status === 401 || jqXHR.status === 403) { - chrome.runtime.sendMessage({ command: 'logout' }); + function handleError(errorCallback, jqXHR, tokenError) { + if (tokenError || jqXHR.status === 401 || jqXHR.status === 403) { + chrome.runtime.sendMessage({ command: 'logout', expired: true }); return; } errorCallback(new ErrorResponse(jqXHR)); } + + function handleTokenState(self) { + var deferred = Q.defer(); + self.tokenService.getToken(function (accessToken) { + if (self.tokenService.tokenNeedsRefresh()) { + self.tokenService.getRefreshToken(function (refreshToken) { + $.ajax({ + type: 'POST', + url: self.baseUrl + '/connect/token', + data: { + grant_type: 'refresh_token', + client_id: 'browser', + refresh_token: refreshToken + }, + contentType: 'application/x-www-form-urlencoded; charset=utf-8', + dataType: 'json', + success: function (response) { + var token = new IdentityTokenResponse(response); + tokenService.setTokens(token.accessToken, token.refreshToken, function () { + deferred.resolve(token.accessToken); + }); + }, + error: function (jqXHR, textStatus, errorThrown) { + deferred.reject(jqXHR); + } + }); + }); + } + else { + deferred.resolve(accessToken); + } + }); + + return deferred.promise + } }; diff --git a/src/services/tokenService.js b/src/services/tokenService.js index 70e7087095..5126d97cf6 100644 --- a/src/services/tokenService.js +++ b/src/services/tokenService.js @@ -145,6 +145,23 @@ function initTokenService() { return !(d.valueOf() > (new Date().valueOf() + (offsetSeconds * 1000))); }; + TokenService.prototype.tokenSecondsRemaining = function (offsetSeconds) { + var d = this.getTokenExpirationDate(); + offsetSeconds = offsetSeconds || 0; + if (d === null) { + return 0; + } + + var msRemaining = d.valueOf() - (new Date().valueOf() + (offsetSeconds * 1000)); + return Math.round(msRemaining / 1000); + }; + + TokenService.prototype.tokenNeedsRefresh = function (minutes) { + minutes = minutes || 5; // default 5 minutes + var sRemaining = this.tokenSecondsRemaining(); + return sRemaining < (60 * minutes); + }; + TokenService.prototype.isTwoFactorScheme = function () { return this.getScheme() !== 'Application'; };