From e8f0471bcfe8516fdb1afb1efcc34e9c4053e9ee Mon Sep 17 00:00:00 2001 From: Francis Lachapelle Date: Thu, 7 May 2020 16:45:37 -0400 Subject: [PATCH] feat(core(js)): improve Google Authenticator on login page, add QR code Closes #2722 --- UI/MainUI/English.lproj/Localizable.strings | 6 +++ .../English.lproj/Localizable.strings | 5 ++ UI/Templates/MainUI/SOGoRootPage.wox | 52 ++++++++++++++----- UI/Templates/PreferencesUI/UIxPreferences.wox | 26 +++++----- UI/WebServerResources/Gruntfile.js | 3 +- .../js/Common/sgQrCode.directive.js | 41 +++++++++++++++ UI/WebServerResources/js/Main/Main.app.js | 14 +++-- UI/WebServerResources/package.json | 3 +- 8 files changed, 116 insertions(+), 34 deletions(-) create mode 100644 UI/WebServerResources/js/Common/sgQrCode.directive.js diff --git a/UI/MainUI/English.lproj/Localizable.strings b/UI/MainUI/English.lproj/Localizable.strings index eb9f65afd..1ff8b2d31 100644 --- a/UI/MainUI/English.lproj/Localizable.strings +++ b/UI/MainUI/English.lproj/Localizable.strings @@ -21,6 +21,12 @@ "browserNotCompatible" = "We've detected that your browser version is currently not supported on this site. Our recommendation is to use Firefox. Click on the link below to download the most current version of this browser."; "alternativeBrowsers" = "Alternatively, you can also use the following compatible browsers"; "alternativeBrowserSafari" = "Alternatively, you can also use Safari."; + +/* 2FA */ +"Verification Code" = "Verification Code"; +"Enter the 6-digit verification code from your Google Authenticator application." = "Enter the 6-digit verification code from your Google Authenticator application."; +"You provided an invalid Google Authenticator key." = "You provided an invalid Google Authenticator key."; + "Download" = "Download"; "Language" = "Language"; "choose" = "Choose ..."; diff --git a/UI/PreferencesUI/English.lproj/Localizable.strings b/UI/PreferencesUI/English.lproj/Localizable.strings index 859405fad..106b00d9a 100644 --- a/UI/PreferencesUI/English.lproj/Localizable.strings +++ b/UI/PreferencesUI/English.lproj/Localizable.strings @@ -448,6 +448,11 @@ "animation_LIMITED" = "Limited"; "animation_NONE" = "None"; +/* 2FA */ +"Enable two-factor authentication using Google Authenticator" = "Enable two-factor authentication using Google Authenticator"; +"You must enter this key into your Google Authenticator application." = "You must enter this key into your Google Authenticator application."; +"If you do not and you log out you will not be able to login again." = "If you do not and you log out you will not be able to login again."; + /* External Sieve scripts */ "An external Sieve script is active" = "An external Sieve script is active"; "Sieve is a programming language that can be used for email filtering. If you let SOGo handle your filters, vacation and forward settings, your active script will be disabled." = "Sieve is a programming language that can be used for email filtering. If you let SOGo handle your filters, vacation and forward settings, your active script will be disabled."; diff --git a/UI/Templates/MainUI/SOGoRootPage.wox b/UI/Templates/MainUI/SOGoRootPage.wox index dc6babab3..7912d1d62 100644 --- a/UI/Templates/MainUI/SOGoRootPage.wox +++ b/UI/Templates/MainUI/SOGoRootPage.wox @@ -52,16 +52,6 @@ - - - - email - - - -
language @@ -112,13 +102,13 @@ + ng-if="!app.loginState" + ng-disabled="loginForm.$invalid" + sg-ripple-click="loginContent"> arrow_forward
- - + + +
+
+ + + lock + +
+
+
+ + arrow_backward + + + arrow_forward + +
+
+
+
+
@@ -152,10 +172,14 @@
{{app.errorMessage}}
- +
+ + diff --git a/UI/Templates/PreferencesUI/UIxPreferences.wox b/UI/Templates/PreferencesUI/UIxPreferences.wox index dbb2c7d8f..2b42fccc1 100644 --- a/UI/Templates/PreferencesUI/UIxPreferences.wox +++ b/UI/Templates/PreferencesUI/UIxPreferences.wox @@ -8,7 +8,7 @@ xmlns:label="OGo:label" className="UIxPageFrame" title="title" - const:jsFiles="vendor/ckeditor/ckeditor.js, vendor/ckeditor/ck.js, vendor/angular-file-upload.min.js, vendor/ng-sortable.min.js, Common.js, Preferences.js, Preferences.services.js, Mailer.services.js, Contacts.services.js"> + const:jsFiles="vendor/ckeditor/ckeditor.js, vendor/ckeditor/ck.js, vendor/angular-file-upload.min.js, vendor/ng-sortable.min.js, vendor/qrcode.min.js, Common.js, Preferences.js, Preferences.services.js, Mailer.services.js, Contacts.services.js">
@@ -232,24 +232,22 @@ - - - +
+
+ +
+
+ +
+
diff --git a/UI/WebServerResources/Gruntfile.js b/UI/WebServerResources/Gruntfile.js index 93dec4875..7bfc164be 100644 --- a/UI/WebServerResources/Gruntfile.js +++ b/UI/WebServerResources/Gruntfile.js @@ -139,7 +139,8 @@ module.exports = function(grunt) { '<%= src %>/angular-ui-router/release/angular-ui-router{,.min}.js{,.map}', //'<%= src %>/ng-file-upload/ng-file-upload{,.min}.js{,map}', '<%= src %>/ng-sortable/dist/ng-sortable.min.js{,map}', - '<%= src %>/lodash/lodash{,.min}.js' + '<%= src %>/lodash/lodash{,.min}.js', + '<%= src %>/qrcodejs/qrcode{,.min}.js' ]; for (var j = 0; j < js.length; j++) { var files = grunt.file.expand(grunt.template.process(js[j], {data: options})); diff --git a/UI/WebServerResources/js/Common/sgQrCode.directive.js b/UI/WebServerResources/js/Common/sgQrCode.directive.js new file mode 100644 index 000000000..61c745139 --- /dev/null +++ b/UI/WebServerResources/js/Common/sgQrCode.directive.js @@ -0,0 +1,41 @@ +/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ + +(function() { + /* jshint validthis: true, newcap: false */ + 'use strict'; + + /** + * sgQrCode - Build a otpauth URI and generate a QR Code for the provided secret. + * @see {@link https://davidshimjs.github.io/qrcodejs/|QRCode.js} + * @memberof SOGo.Common + * @example: + + */ + sgQrCode.$inject = ['sgSettings']; + function sgQrCode(Settings) { + return { + restrict: 'E', + scope: { + text: '@', + width: '@', + height: '@' + }, + link: link + }; + + function link(scope, element, attrs) { + var width = parseInt(scope.width) || 256, + height = parseInt(scope.height) || width, + uri = 'otpauth://totp/SOGo:' + Settings.activeUser('email') + '?secret=' + scope.text.replace(/=+$/, '') + '&issuer=SOGo'; + new QRCode(element[0], { + text: uri, + width: width, + height: height + }); + } + } + + angular + .module('SOGo.Common') + .directive('sgQrCode', sgQrCode); +})(); diff --git a/UI/WebServerResources/js/Main/Main.app.js b/UI/WebServerResources/js/Main/Main.app.js index d4748d6e3..a3e37a969 100644 --- a/UI/WebServerResources/js/Main/Main.app.js +++ b/UI/WebServerResources/js/Main/Main.app.js @@ -23,7 +23,9 @@ if (/\blanguage=/.test($window.location.search)) this.creds.language = $window.language; this.loginState = false; - this.showGoogleAuthenticatorCode = false; + + // Code pattern for Google verification code + this.verificationCodePattern = '\\d{6}'; // Show login once everything is initialized this.showLogin = false; @@ -35,9 +37,8 @@ Authentication.login(vm.creds) .then(function(data) { - if (typeof data.gamissingkey != 'undefined' && data.gamissingkey == 1) { - vm.showGoogleAuthenticatorCode = true; - vm.loginState = 'error'; + if (data.gamissingkey) { + vm.loginState = 'googleauthenticatorcode'; } else { vm.loginState = 'logged'; @@ -58,6 +59,11 @@ return false; }; + this.restoreLogin = function() { + vm.loginState = false; + delete vm.creds.verificationCode; + }; + this.showAbout = function($event) { $mdDialog.show({ targetEvent: $event, diff --git a/UI/WebServerResources/package.json b/UI/WebServerResources/package.json index 8ffdddb23..54018b047 100644 --- a/UI/WebServerResources/package.json +++ b/UI/WebServerResources/package.json @@ -30,7 +30,8 @@ "grunt-postcss": ">=0.6.0", "grunt-sass": "^3.1.0", "kss": "^3.0.1", - "node-sass": "^4.14.0", + "node-sass": "^4.14.1", + "qrcodejs": "^1.0.0", "time-grunt": "latest" }, "browserslist": [