diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json index 859bb41db3b..52b94963601 100644 --- a/src/_locales/en/messages.json +++ b/src/_locales/en/messages.json @@ -131,10 +131,6 @@ "message": "Verification Code", "description": "Verification Code" }, - "enterTwoStepVerCode": { - "message": "Enter your two-step verification code.", - "description": "Enter your two-step verification code." - }, "account": { "message": "Account", "description": "Account" diff --git a/src/images/two-factor/u2fkey.jpg b/src/images/two-factor/u2fkey.jpg new file mode 100644 index 00000000000..8013df0e569 Binary files /dev/null and b/src/images/two-factor/u2fkey.jpg differ diff --git a/src/images/two-factor/yubikey.jpg b/src/images/two-factor/yubikey.jpg new file mode 100644 index 00000000000..9ddf755decc Binary files /dev/null and b/src/images/two-factor/yubikey.jpg differ diff --git a/src/models/api/requestModels.js b/src/models/api/requestModels.js index 97d5801eb0f..88a362ba60e 100644 --- a/src/models/api/requestModels.js +++ b/src/models/api/requestModels.js @@ -13,11 +13,12 @@ var FolderRequest = function (folder) { this.name = folder.name ? folder.name.encryptedString : null; }; -var TokenRequest = function (email, masterPasswordHash, provider, token, device) { +var TokenRequest = function (email, masterPasswordHash, provider, token, remember, device) { this.email = email; this.masterPasswordHash = masterPasswordHash; this.token = token; this.provider = provider; + this.remember = remember || remember !== false; this.device = null; if (device) { this.device = device; @@ -42,6 +43,7 @@ var TokenRequest = function (email, masterPasswordHash, provider, token, device) if (this.token && this.provider != null && (typeof this.provider !== 'undefined')) { obj.twoFactorToken = this.token; obj.twoFactorProvider = this.provider; + obj.twoFactorRemember = this.remember ? '1' : '0'; } return obj; @@ -60,6 +62,11 @@ var PasswordHintRequest = function (email) { this.email = email; }; +var TwoFactorEmailRequest = function (email, masterPasswordHash) { + this.email = email; + this.masterPasswordHash = masterPasswordHash; +}; + var DeviceTokenRequest = function () { this.pushToken = null; }; diff --git a/src/models/api/responseModels.js b/src/models/api/responseModels.js index 76b8b9dca5e..1f64e33d8b1 100644 --- a/src/models/api/responseModels.js +++ b/src/models/api/responseModels.js @@ -72,6 +72,7 @@ var IdentityTokenResponse = function (response) { this.privateKey = response.PrivateKey; this.key = response.Key; + this.twoFactorToken = response.TwoFactorToken; }; var ListResponse = function (data) { diff --git a/src/popup/app/accounts/accountsLoginTwoFactorController.js b/src/popup/app/accounts/accountsLoginTwoFactorController.js index 6c98f68d59e..636b06cf214 100644 --- a/src/popup/app/accounts/accountsLoginTwoFactorController.js +++ b/src/popup/app/accounts/accountsLoginTwoFactorController.js @@ -1,7 +1,7 @@ angular .module('bit.accounts') - .controller('accountsLoginTwoFactorController', function ($scope, $state, authService, toastr, utilsService, + .controller('accountsLoginTwoFactorController', function ($scope, $state, authService, toastr, utilsService, SweetAlert, $analytics, i18nService, $stateParams, $filter, constantsService, $timeout, $window, cryptoService, apiService) { $scope.i18n = i18nService; utilsService.initListSectionItemListeners($(document), angular); @@ -11,11 +11,17 @@ var masterPassword = $stateParams.masterPassword; var providers = $stateParams.providers; + if (!email || !masterPassword || !providers) { + $state.go('login'); + return; + } + + $scope.providerType = $stateParams.provider ? $stateParams.provider : getDefaultProvider(providers); $scope.twoFactorEmail = null; $scope.token = null; $scope.constantsProvider = constants.twoFactorProvider; - $scope.providerType = $stateParams.provider ? $stateParams.provider : getDefaultProvider(providers); $scope.u2fReady = false; + $scope.remember = { checked: false }; init(); $scope.loginPromise = null; @@ -25,7 +31,12 @@ return; } - $scope.loginPromise = authService.logIn(email, masterPassword, $scope.providerType, token); + if ($scope.providerType === constants.twoFactorProvider.email || + $scope.providerType === constants.twoFactorProvider.authenticator) { + token = token.replace(' ', '') + } + + $scope.loginPromise = authService.logIn(email, masterPassword, $scope.providerType, token, $scope.remember.checked); $scope.loginPromise.then(function () { $analytics.eventTrack('Logged In From Two-step'); $state.go('tabs.vault', { animation: 'in-slide-left', syncOnLoad: true }); @@ -43,16 +54,15 @@ } var key = cryptoService.makeKey(masterPassword, email); - var hash = cryptoService.hashPassword(masterPassword, key); - apiService.postTwoFactorEmail({ - email: email, - masterPasswordHash: hash - }, function () { - if (doToast) { - toastr.success('Verification email sent to ' + $scope.twoFactorEmail + '.'); - } - }, function () { - toastr.error('Could not send verification email.'); + cryptoService.hashPassword(masterPassword, key, function (hash) { + var request = new TwoFactorEmailRequest(email, hash); + apiService.postTwoFactorEmail(request, function () { + if (doToast) { + toastr.success('Verification email sent to ' + $scope.twoFactorEmail + '.'); + } + }, function () { + toastr.error('Could not send verification email.'); + }); }); }; @@ -131,7 +141,27 @@ else if ($scope.providerType === constants.twoFactorProvider.email) { var params = providers[constants.twoFactorProvider.email]; $scope.twoFactorEmail = params.Email; - if (Object.keys(providers).length > 1) { + + if (chrome.extension.getViews({ type: 'popup' }).length > 0) { + SweetAlert.swal({ + title: 'Two-step Login', + text: 'Clicking outside the popup window to check your email for your verification code will ' + + 'cause this popup to close. ' + + 'Do you want to open this popup in a new window so that it does not close?', + showCancelButton: true, + confirmButtonText: i18nService.yes, + cancelButtonText: i18nService.no + }, function (confirmed) { + if (confirmed) { + chrome.tabs.create({ url: '/popup/index.html#!/login' }); + return; + } + else if (Object.keys(providers).length > 1) { + $scope.sendEmail(false); + } + }); + } + else if (Object.keys(providers).length > 1) { $scope.sendEmail(false); } } diff --git a/src/popup/app/accounts/views/accountsLoginTwoFactor.html b/src/popup/app/accounts/views/accountsLoginTwoFactor.html index 3601e35df98..b34f20c8285 100644 --- a/src/popup/app/accounts/views/accountsLoginTwoFactor.html +++ b/src/popup/app/accounts/views/accountsLoginTwoFactor.html @@ -11,6 +11,14 @@
{{i18n.verificationCode}}
+
+

+ Enter the 6 digit verification code from your authenticator app. +

+

+ Enter the 6 digit verification code that was emailed to {{twoFactorEmail}}. +

+
@@ -21,14 +29,14 @@
- +
-
+

+ Send verification code email again +

Use another two-step login method

@@ -61,6 +69,10 @@
YubiKey
+
+

Insert your YubiKey into your computer's USB port, then touch its button.

+ +
@@ -71,12 +83,9 @@
- +
-

@@ -96,10 +105,27 @@

FIDO U2F
-
Loading...
-
Touch button
- -

+

+ +

Loading...

+
+

+ Insert your Security Key into your computer's USB port. If it has a button, touch it. +

+ +
+
+
+
+
+
+ + +
+
+
+
+

Use another two-step login method

diff --git a/src/popup/app/services/authService.js b/src/popup/app/services/authService.js index 1f0cf0f49d6..2ae76daceda 100644 --- a/src/popup/app/services/authService.js +++ b/src/popup/app/services/authService.js @@ -2,50 +2,69 @@ .module('bit.services') .factory('authService', function (cryptoService, apiService, userService, tokenService, $q, $rootScope, loginService, - folderService, settingsService, syncService, appIdService, utilsService) { + folderService, settingsService, syncService, appIdService, utilsService, constantsService) { var _service = {}; - _service.logIn = function (email, masterPassword, twoFactorProvider, twoFactorToken) { + _service.logIn = function (email, masterPassword, twoFactorProvider, twoFactorToken, remember) { email = email.toLowerCase(); var key = cryptoService.makeKey(masterPassword, email); var deferred = $q.defer(); cryptoService.hashPassword(masterPassword, key, function (hashedPassword) { appIdService.getAppId(function (appId) { - var deviceRequest = new DeviceRequest(appId, utilsService); - var request = new TokenRequest(email, hashedPassword, twoFactorProvider, twoFactorToken, deviceRequest); + tokenService.getTwoFactorToken(email, function (twoFactorRememberedToken) { + var deviceRequest = new DeviceRequest(appId, utilsService); + var request; - apiService.postIdentityToken(request, function (response) { - // success - if (!response || !response.accessToken) { - return; + if (twoFactorToken && typeof (twoFactorProvider) !== 'undefined' && twoFactorProvider !== null) { + request = new TokenRequest(email, hashedPassword, twoFactorProvider, twoFactorToken, remember, + deviceRequest); + } + else if (twoFactorRememberedToken) { + request = new TokenRequest(email, hashedPassword, constantsService.twoFactorProvider.remember, + twoFactorRememberedToken, false, deviceRequest); + } + else { + request = new TokenRequest(email, hashedPassword, null, null, false, deviceRequest); } - tokenService.setTokens(response.accessToken, response.refreshToken, function () { - cryptoService.setKey(key, function () { - cryptoService.setKeyHash(hashedPassword, function () { - userService.setUserIdAndEmail(tokenService.getUserId(), tokenService.getEmail(), function () { - cryptoService.setEncKey(response.key).then(function () { - return cryptoService.setEncPrivateKey(response.privateKey); - }).then(function () { - chrome.runtime.sendMessage({ command: 'loggedIn' }); - deferred.resolve({ - twoFactor: false, - twoFactorProviders: null + apiService.postIdentityToken(request, function (response) { + // success + if (!response || !response.accessToken) { + return; + } + + if (response.twoFactorToken) { + tokenService.setTwoFactorToken(response.twoFactorToken, email, function () { }); + } + + tokenService.setTokens(response.accessToken, response.refreshToken, function () { + cryptoService.setKey(key, function () { + cryptoService.setKeyHash(hashedPassword, function () { + userService.setUserIdAndEmail(tokenService.getUserId(), tokenService.getEmail(), + function () { + cryptoService.setEncKey(response.key).then(function () { + return cryptoService.setEncPrivateKey(response.privateKey); + }).then(function () { + chrome.runtime.sendMessage({ command: 'loggedIn' }); + deferred.resolve({ + twoFactor: false, + twoFactorProviders: null + }); + }); }); - }); }); }); }); + }, function (providers) { + // two factor required + deferred.resolve({ + twoFactor: true, + twoFactorProviders: providers + }); + }, function (error) { + // error + deferred.reject(error); }); - }, function (providers) { - // two factor required - deferred.resolve({ - twoFactor: true, - twoFactorProviders: providers - }); - }, function (error) { - // error - deferred.reject(error); }); }); }); diff --git a/src/popup/less/pages.less b/src/popup/less/pages.less index efdcb870392..e5ca95ec57b 100644 --- a/src/popup/less/pages.less +++ b/src/popup/less/pages.less @@ -65,3 +65,13 @@ margin: 0 auto; } } + +.two-factor-key-page { + padding: 20px 20px 0 20px; + text-align: center; + + .img-responsive { + margin-left: auto; + margin-right: auto; + } +} \ No newline at end of file diff --git a/src/services/apiService.js b/src/services/apiService.js index 8cbf1dff39b..a1db49b7899 100644 --- a/src/services/apiService.js +++ b/src/services/apiService.js @@ -45,7 +45,9 @@ function initApiService() { error: function (jqXHR, textStatus, errorThrown) { if (jqXHR.responseJSON && jqXHR.responseJSON.TwoFactorProviders2 && Object.keys(jqXHR.responseJSON.TwoFactorProviders2).length) { - successWithTwoFactor(jqXHR.responseJSON.TwoFactorProviders2); + self.tokenService.clearTwoFactorToken(tokenRequest.email, function () { + successWithTwoFactor(jqXHR.responseJSON.TwoFactorProviders2); + }); } else { error(new ErrorResponse(jqXHR, true)); @@ -56,12 +58,14 @@ function initApiService() { // Two Factor APIs - ApiService.prototype.postTwoFactorEmail = function (success, error) { + ApiService.prototype.postTwoFactorEmail = function (request, success, error) { var self = this; $.ajax({ type: 'POST', - url: self.baseUrl + '/two-factor/send-email?' + token, - dataType: 'json', + url: self.baseUrl + '/two-factor/send-email-login', + dataType: 'text', + contentType: 'application/json; charset=utf-8', + data: JSON.stringify(request), success: function (response) { success(response); }, diff --git a/src/services/tokenService.js b/src/services/tokenService.js index 439a38718d0..4ee6a115343 100644 --- a/src/services/tokenService.js +++ b/src/services/tokenService.js @@ -102,6 +102,46 @@ function initTokenService() { }); }; + TokenService.prototype.setTwoFactorToken = function (token, email, callback) { + if (!callback || typeof callback !== 'function') { + throw 'callback function required'; + } + + var obj = {}; + obj['twoFactorToken_' + email] = token; + + chrome.storage.local.set(obj, function () { + callback(); + }); + }; + + TokenService.prototype.getTwoFactorToken = function (email, callback) { + if (!callback || typeof callback !== 'function') { + throw 'callback function required'; + } + + var prop = 'twoFactorToken_' + email; + + chrome.storage.local.get(prop, function (obj) { + if (obj && obj[prop]) { + callback(obj[prop]); + return; + } + + return callback(null); + }); + }; + + TokenService.prototype.clearTwoFactorToken = function (email, callback) { + if (!callback || typeof callback !== 'function') { + throw 'callback function required'; + } + + chrome.storage.local.remove('twoFactorToken_' + email, function () { + callback(); + }); + }; + TokenService.prototype.clearAuthBearer = function (callback) { if (!callback || typeof callback !== 'function') { throw 'callback function required';