From 8236056316f53e2c555e7141d2ccfa571fb48f4a Mon Sep 17 00:00:00 2001 From: Nabil Adouani Date: Sat, 2 May 2020 20:00:19 +0200 Subject: [PATCH] #1303 Initial commit of MFA support --- frontend/app/index.html | 3 + frontend/app/scripts/app.js | 1 + .../scripts/controllers/AuthenticationCtrl.js | 8 +- .../app/scripts/controllers/SettingsCtrl.js | 53 +++++++- .../scripts/services/api/AuthenticationSrv.js | 127 +++++++++--------- frontend/app/scripts/services/api/UserSrv.js | 16 +++ frontend/app/views/login.html | 10 ++ .../app/views/partials/personal-settings.html | 68 +++++++++- frontend/bower.json | 4 +- frontend/test/karma.conf.js | 3 + 10 files changed, 226 insertions(+), 67 deletions(-) diff --git a/frontend/app/index.html b/frontend/app/index.html index 906973bfff..2020ac526b 100644 --- a/frontend/app/index.html +++ b/frontend/app/index.html @@ -112,6 +112,9 @@ + + + diff --git a/frontend/app/scripts/app.js b/frontend/app/scripts/app.js index fb57257ce0..e0ac4d9f34 100644 --- a/frontend/app/scripts/app.js +++ b/frontend/app/scripts/app.js @@ -30,6 +30,7 @@ angular.module('thehive', [ 'dndLists', 'colorpicker.module', 'btorfs.multiselect', + 'ja.qr', 'theHiveControllers', 'theHiveServices', 'theHiveFilters', diff --git a/frontend/app/scripts/controllers/AuthenticationCtrl.js b/frontend/app/scripts/controllers/AuthenticationCtrl.js index 64233d2565..84a79eb809 100644 --- a/frontend/app/scripts/controllers/AuthenticationCtrl.js +++ b/frontend/app/scripts/controllers/AuthenticationCtrl.js @@ -5,7 +5,9 @@ 'use strict'; angular.module('theHiveControllers') .controller('AuthenticationCtrl', function($rootScope, $scope, $state, $location, $uibModalStack, $stateParams, AuthenticationSrv, NotificationSrv, UtilsSrv, UrlParser, appConfig) { - $scope.params = {}; + $scope.params = { + requireMfa: false + }; $uibModalStack.dismissAll(); @@ -38,13 +40,15 @@ $scope.login = function() { $scope.params.username = $scope.params.username.toLowerCase(); - AuthenticationSrv.login($scope.params.username, $scope.params.password) + AuthenticationSrv.login($scope.params.username, $scope.params.password, $scope.params.mfaCode) .then(function() { $state.go('app.index'); }) .catch(function(err) { if (err.status === 520) { NotificationSrv.error('AuthenticationCtrl', err.data.message, err.status); + } else if(err.status === 402){ + $scope.params.requireMfa = true; } else { NotificationSrv.log(err.data.message, 'error'); } diff --git a/frontend/app/scripts/controllers/SettingsCtrl.js b/frontend/app/scripts/controllers/SettingsCtrl.js index 26b0b57937..39384d42fa 100644 --- a/frontend/app/scripts/controllers/SettingsCtrl.js +++ b/frontend/app/scripts/controllers/SettingsCtrl.js @@ -1,7 +1,7 @@ (function() { 'use strict'; angular.module('theHiveControllers').controller('SettingsCtrl', - function($scope, $state, UserSrv, AuthenticationSrv, NotificationSrv, resizeService, readLocalPicService, currentUser, appConfig) { + function($scope, $state, UserSrv, AuthenticationSrv, NotificationSrv, clipboard, resizeService, readLocalPicService, currentUser, appConfig) { $scope.currentUser = currentUser; $scope.appConfig = appConfig; @@ -23,6 +23,14 @@ password: '', passwordConfirm: '' }; + + $scope.mfaData = { + enabled: $scope.currentUser.hasMFA, + secret: null, + uri: null, + code: null + }; + $scope.canChangePass = appConfig.config.capabilities.indexOf('changePassword') !== -1; @@ -86,6 +94,49 @@ } }; + $scope.copySecret = function(secret) { + clipboard.copyText(secret); + NotificationSrv.success('MFA Secret has been successfully copied to clipboard.'); + }; + + $scope.enableMfa = function() { + if($scope.mfaData.enabled) { + // Fetch the secret + UserSrv.fetchMfaSecret() + .then(function(response) { + $scope.mfaData.secret = response.data.secret; + $scope.mfaData.uri = response.data.uri; + }) + .catch(function(err) { + NotificationSrv.error('SettingsCtrl', err.data, err.status); + }); + } + }; + + $scope.setMfaSettings = function(form) { + UserSrv.setMfa($scope.mfaData.code) + .then(function(/*response*/) { + NotificationSrv.log('Your multi-factor authentication has been successfully configured', 'success'); + $state.reload(); + }) + .catch(function(/*err*/) { + $scope.mfaData.enabled = true; + form.mfaCode.$setValidity('mfaInvalid', false); + }); + }; + + $scope.resetMfa = function() { + UserSrv.resetMfa() + .then(function(/*response*/) { + NotificationSrv.log('Your multi-factor authentication has been successfully disabled', 'success'); + $state.reload(); + + }) + .catch(function(err) { + NotificationSrv.error('SettingsCtrl', err.data, err.status); + }); + }; + $scope.clearPassword = function(form, changePassword) { if (!changePassword) { diff --git a/frontend/app/scripts/services/api/AuthenticationSrv.js b/frontend/app/scripts/services/api/AuthenticationSrv.js index 33b8fe8ec6..88c8269e62 100644 --- a/frontend/app/scripts/services/api/AuthenticationSrv.js +++ b/frontend/app/scripts/services/api/AuthenticationSrv.js @@ -1,70 +1,75 @@ (function() { - 'use strict'; - angular - .module('theHiveServices') - .factory('AuthenticationSrv', function($http, $q, UtilsSrv, SecuritySrv) { - var self = { - currentUser: null, - login: function(username, password) { - return $http - .post('./api/login', { - user: username, - password: password - }); - }, - logout: function(success, failure) { - $http - .get('./api/logout') - .then(function(data, status, headers, config) { - self.currentUser = null; + 'use strict'; + angular + .module('theHiveServices') + .factory('AuthenticationSrv', function($http, $q, UtilsSrv, SecuritySrv) { + var self = { + currentUser: null, + login: function(username, password, code) { + var post = { + user: username, + password: password + }; - if (angular.isFunction(success)) { - success(data, status, headers, config); - } - }) - .catch(function(data, status, headers, config) { - if (angular.isFunction(failure)) { - failure(data, status, headers, config); - } - }); - }, - current: function() { - var result = {}; - return $http - .get('./api/v1/user/current') - .then(function(response) { - self.currentUser = response.data; - UtilsSrv.shallowClearAndCopy(response.data, result); + if(code) { + post.code = code; + } - return $q.resolve(result); - }) - .catch(function(err) { - self.currentUser = null; - return $q.reject(err); - }); - }, - ssoLogin: function(code, state) { - var url = angular.isDefined(code) && angular.isDefined(state) ? './api/ssoLogin?code=' + code + '&state=' + state : './api/ssoLogin'; - return $http.post(url, {}); - }, - isSuperAdmin: function() { - var user = self.currentUser; + return $http.post('./api/login', post); + }, + logout: function(success, failure) { + $http + .get('./api/logout') + .then(function(data, status, headers, config) { + self.currentUser = null; - return user && user.organisation === 'admin'; - }, - hasPermission: function(permissions) { - var user = self.currentUser; + if (angular.isFunction(success)) { + success(data, status, headers, config); + } + }) + .catch(function(data, status, headers, config) { + if (angular.isFunction(failure)) { + failure(data, status, headers, config); + } + }); + }, + current: function() { + var result = {}; + return $http + .get('./api/v1/user/current') + .then(function(response) { + self.currentUser = response.data; + UtilsSrv.shallowClearAndCopy(response.data, result); - if (!user) { - return false; - } + return $q.resolve(result); + }) + .catch(function(err) { + self.currentUser = null; + return $q.reject(err); + }); + }, + ssoLogin: function(code, state) { + var url = angular.isDefined(code) && angular.isDefined(state) ? './api/ssoLogin?code=' + code + '&state=' + state : './api/ssoLogin'; + return $http.post(url, {}); + }, + isSuperAdmin: function() { + var user = self.currentUser; - //return !_.isEmpty(_.intersection(user.permissions, permissions)); + return user && user.organisation === 'admin'; + }, + hasPermission: function(permissions) { + var user = self.currentUser; - return SecuritySrv.checkPermissions(user.permissions, permissions); - } - }; + if (!user) { + return false; + } - return self; - }); + //return !_.isEmpty(_.intersection(user.permissions, permissions)); + + return SecuritySrv.checkPermissions(user.permissions, permissions); + } + }; + + return self; + }); })(); diff --git a/frontend/app/scripts/services/api/UserSrv.js b/frontend/app/scripts/services/api/UserSrv.js index 3764a46cb6..28a5d667db 100644 --- a/frontend/app/scripts/services/api/UserSrv.js +++ b/frontend/app/scripts/services/api/UserSrv.js @@ -156,6 +156,22 @@ }); }; + this.fetchMfaSecret = function() { + return $http + .post('./api/v1/auth/totp/set', {}); + }; + + this.setMfa = function(code) { + return $http + .post('./api/v1/auth/totp/set', { + code: code + }); + }; + + this.resetMfa = function() { + return $http.post('./api/v1/auth/totp/unset'); + }; + this.list = function(organisation, query) { // var post = { // range: 'all', diff --git a/frontend/app/views/login.html b/frontend/app/views/login.html index 0c30c09a84..16656c0f0d 100644 --- a/frontend/app/views/login.html +++ b/frontend/app/views/login.html @@ -13,6 +13,16 @@ +
+
+ Please provide MFA code +
+
+ + +
+
+
diff --git a/frontend/app/views/partials/personal-settings.html b/frontend/app/views/partials/personal-settings.html index 2d3d93ee9d..5a14eb6276 100644 --- a/frontend/app/views/partials/personal-settings.html +++ b/frontend/app/views/partials/personal-settings.html @@ -1,5 +1,5 @@
-
+

Settings

@@ -136,8 +136,69 @@

-
+ + +
+
+

+ + Enable Multi-Factor Authentication +

+
+
+
+ Your have enable Multi-Factor Authentication, do you want to disabled it? +
+ +
+
+ +
+
+ Your are going to enable Multi-Factor Authentication. + Use the QRCode or the Secret to generate a MFA code and submit it. +
+ +
+ +
+ +
+

{{mfaData.secret}}

+ +
+
+ +
+ + +
+
+
+
+ +
+ + + +
+ + +  Required field + + +
+
+
+

@@ -159,4 +220,7 @@

Oops, that's not the same password as the first one

+
+ Oops, that's not a valid MFA code +
diff --git a/frontend/bower.json b/frontend/bower.json index d669c4a07a..6dd5f7f127 100644 --- a/frontend/bower.json +++ b/frontend/bower.json @@ -52,7 +52,9 @@ "file-saver": "1.3.4", "js-url": "~2.5.3", "bootstrap-sass": "bootstrap-sass-official#^3.4.1", - "angular-bootstrap-multiselect": "https://github.com/bentorfs/angular-bootstrap-multiselect.git#^1.1.11" + "angular-bootstrap-multiselect": "https://github.com/bentorfs/angular-bootstrap-multiselect.git#^1.1.11", + "qrcode.js": "qrcode#^1.0.2", + "angular-qr": "^0.2.0" }, "devDependencies": { "angular-mocks": "1.7.8" diff --git a/frontend/test/karma.conf.js b/frontend/test/karma.conf.js index b0fb86f72b..1acdb328e1 100644 --- a/frontend/test/karma.conf.js +++ b/frontend/test/karma.conf.js @@ -71,6 +71,9 @@ module.exports = function(config) { 'bower_components/js-url/url.js', 'bower_components/bootstrap-sass/assets/javascripts/bootstrap.js', 'bower_components/angular-bootstrap-multiselect/dist/angular-bootstrap-multiselect.js', + 'bower_components/qrcode.js/lib/qrcode.js', + 'bower_components/qrcode/lib/qrcode.js', + 'bower_components/angular-qr/src/angular-qr.js', 'bower_components/angular-mocks/angular-mocks.js', // endbower "bower_components/cryptojslib/components/core-min.js",