feat(password-recovery): Add password recovery with secret question or secondary email

This commit is contained in:
smizrahi
2022-10-03 08:56:41 +02:00
parent 6931186bba
commit 656807b77d
47 changed files with 1729 additions and 159 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -20,6 +20,8 @@
PolicyPasswordTooShort: 6,
PolicyPasswordTooYoung: 7,
PolicyPasswordInHistory: 8,
PolicyPasswordRecoveryFailed: 9,
PolicyPasswordRecoveryInvalidToken: 10,
PolicyNoError: 65535
})
@@ -168,7 +170,7 @@
return d.promise;
}, // login: function(data) { ...
changePassword: function(userName, domain, newPassword, oldPassword) {
changePassword: function(userName, domain, newPassword, oldPassword, token) {
var d = $q.defer(),
xsrfCookie = $cookies.get('XSRF-TOKEN');
@@ -180,7 +182,7 @@
headers: {
'X-XSRF-TOKEN' : xsrfCookie
},
data: { userName: userName, newPassword: newPassword, oldPassword: oldPassword }
data: { userName: userName, newPassword: newPassword, oldPassword: oldPassword, token: token }
}).then(function() {
d.resolve({url: redirectUrl(userName, domain)});
}, function(response) {
@@ -205,6 +207,8 @@
error = l("Password change failed - Password is too young");
} else if (perr == passwordPolicyConfig.PolicyPasswordInHistory) {
error = l("Password change failed - Password is in history");
} else if (perr == passwordPolicyConfig.PolicyPasswordRecoveryInvalidToken) {
error = l("Invalid token. Could not change password");
} else {
error = l("Unhandled policy error: %{0}").formatted(perr);
perr = passwordPolicyConfig.PolicyPasswordUnknown;
@@ -215,6 +219,113 @@
d.reject(error);
});
return d.promise;
},
passwordRecovery: function (userName, domain) {
const self = this;
var d = $q.defer(),
xsrfCookie = $cookies.get('XSRF-TOKEN');
$cookies.remove('XSRF-TOKEN', { path: '/SOGo/' });
$http({
method: 'POST',
url: '/SOGo/so/passwordRecovery',
headers: {
'X-XSRF-TOKEN': xsrfCookie
},
data: { userName: userName, domain: domain }
}).then(function (response) {
d.resolve(Object.assign(
{ url: redirectUrl(userName, domain) },
response.data,
'SecretQuestion' === response.data.mode ? { secretQuestionLabel: l('passwordRecovery_' + response.data.secretQuestion) } : {},
'SecondaryEmail' === response.data.mode ? { obfuscatedRecoveryEmail: response.data.obfuscatedSecondaryEmail } : {}
));
}, function () {
// Restore the cookie
$cookies.put('XSRF-TOKEN', xsrfCookie, { path: '/SOGo/' });
d.reject(l("Unhandled policy error: %{0}").formatted(passwordPolicyConfig.PolicyPasswordRecoveryFailed));
});
return d.promise;
},
passwordRecoveryEmail: function (userName, domain, mode, mailDomain) {
const self = this;
var d = $q.defer(),
xsrfCookie = $cookies.get('XSRF-TOKEN');
$cookies.remove('XSRF-TOKEN', { path: '/SOGo/' });
$http({
method: 'POST',
url: '/SOGo/so/passwordRecoveryEmail',
headers: {
'X-XSRF-TOKEN': xsrfCookie
},
data: { userName: userName, domain: domain, mode: mode, mailDomain: mailDomain }
}).then(function (response) {
d.resolve(response.data.jwt);
}, function (response) {
// Restore the cookie
$cookies.put('XSRF-TOKEN', xsrfCookie, { path: '/SOGo/' });
d.reject(l(response.data));
});
return d.promise;
},
passwordRecoveryCheck: function (userName, domain, mode, question, answer, mailDomain) {
const self = this;
var d = $q.defer(),
xsrfCookie = $cookies.get('XSRF-TOKEN');
$cookies.remove('XSRF-TOKEN', { path: '/SOGo/' });
$http({
method: 'POST',
url: '/SOGo/so/passwordRecoveryCheck',
headers: {
'X-XSRF-TOKEN': xsrfCookie
},
data: { userName: userName, domain: domain, mode: mode, question: question, answer: answer, mailDomain: mailDomain }
}).then(function (response) {
d.resolve(response.data.jwt);
}, function (response) {
// Restore the cookie
$cookies.put('XSRF-TOKEN', xsrfCookie, { path: '/SOGo/' });
d.reject(l(response.data));
});
return d.promise;
},
passwordRecoveryEnabled: function (userName, domain) {
const self = this;
var d = $q.defer(),
xsrfCookie = $cookies.get('XSRF-TOKEN');
$cookies.remove('XSRF-TOKEN', { path: '/SOGo/' });
$http({
method: 'POST',
url: '/SOGo/so/passwordRecoveryEnabled',
headers: {
'X-XSRF-TOKEN': xsrfCookie
},
data: { userName: userName, domain: domain }
}).then(function (response) {
d.resolve(response.data.domain);
}, function () {
// Restore the cookie
$cookies.put('XSRF-TOKEN', xsrfCookie, { path: '/SOGo/' });
d.reject();
});
return d.promise;
}
};
return service;
@@ -20,14 +20,43 @@
</md-dialog>
*/
sgRippleClick.$inject = ['$log', '$timeout'];
function sgRippleClick($log, $timeout) {
sgRippleClick.$inject = ['$log', '$timeout', '$rootScope'];
function sgRippleClick($log, $timeout, scope) {
function rippleEffect(element, coordinates, container, content) {
// Show ripple
angular.element(container).css({ 'overflow': 'hidden', 'position': 'relative' });
angular.element(content).css({ top: container.scrollTop + 'px' });
element.css({
'top': (coordinates.top - container.offsetTop + container.scrollTop) + 'px',
'left': (coordinates.left - container.offsetLeft) + 'px',
'height': '400vmin',
'width': '400vmin'
});
// Show ripple content
content.classList.remove('ng-hide');
}
scope.$on('sgRippleDo', function (e, containerName) {
const container = document.getElementById(containerName);
container.classList.remove('ng-hide');
rippleEffect(
angular.element(document.querySelector('sg-ripple'))
, { left: (window.innerWidth / 2), top: (window.innerHeight / 2) }
, container
, document.querySelector('sg-ripple-content')
);
});
return {
restrict: 'A',
compile: compile
};
function compile(tElement, tAttrs) {
return function postLink(scope, element, attr) {
@@ -90,26 +119,16 @@
}
if (content.classList.contains('ng-hide')) {
// Show ripple
angular.element(container).css({ 'overflow': 'hidden', 'position': 'relative' });
angular.element(content).css({ top: container.scrollTop + 'px' });
$timeout(function() {
// Wait until next digest for CSS animation to work
ripple.css({
'top': (coordinates.top - container.offsetTop + container.scrollTop) + 'px',
'left': (coordinates.left - container.offsetLeft) + 'px',
'height': '400vmin',
'width': '400vmin'
});
// Show ripple content
content.classList.remove('ng-hide');
rippleEffect(ripple, coordinates, container, content);
});
}
else {
// Hide ripple layer
ripple.css({
'top': (coordinates.top - container.offsetTop + container.scrollTop) + 'px',
'left': (coordinates.left - container.offsetLeft) + 'px',
'left': (coordinates.left - container.offsetLeft) + 'px',
'height': '0px',
'width': '0px'
});
@@ -0,0 +1,25 @@
/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
(function() {
'use strict';
/**
* sgFocus - A service to set the focus on the element associated to a specific string
* @memberof SOGo.Common
* @param {string} name - the string identifier of the element
* @see {@link SOGo.Common.sgRippleClick}
* @ngInject
*/
sgRippleClick.$inject = ['$rootScope', '$timeout'];
function sgRippleClick($rootScope, $timeout) {
return function (containerName) {
$timeout(function() {
$rootScope.$broadcast('sgRippleDo', containerName);
});
};
}
angular
.module('SOGo.Common')
.factory('sgRippleClick', sgRippleClick);
})();
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+149 -8
View File
@@ -1,19 +1,19 @@
/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* JavaScript for MainUI (SOGoRootPage) */
(function() {
'use strict';
angular.module('SOGo.MainUI', ['SOGo.Common', 'SOGo.Authentication']);
const PASSWORD_RECOVERY_TIMER_MS = 2000;
/**
* @ngInject
*/
LoginController.$inject = ['$scope', '$window', '$timeout', 'Dialog', '$mdDialog', 'Authentication', 'sgFocus'];
function LoginController($scope, $window, $timeout, Dialog, $mdDialog, Authentication, focus) {
LoginController.$inject = ['$scope', '$window', '$timeout', 'Dialog', '$mdDialog', 'Authentication', 'sgFocus', 'sgRippleClick'];
function LoginController($scope, $window, $timeout, Dialog, $mdDialog, Authentication, focus, rippleDo) {
var vm = this;
this.$onInit = function() {
this.$onInit = function () {
this.creds = {
username: $window.cookieUsername,
password: null,
@@ -31,9 +31,53 @@
// Password policy - change expired password
this.passwords = { newPassword: null, newPasswordConfirmation: null, oldPassword: null };
// Password recovery
this.passwordRecovery = {
passwordRecoveryEnabled: false,
passwordRecoveryQuestionKey: null,
passwordRecoveryQuestion: null,
passwordRecoveryMode: null,
passwordRecoveryQuestionAnswer: null,
passwordRecoveryToken: null,
passwordRecoveryLinkTimer: null,
passwordRecoverySecondaryEmailText: null,
passwordRecoveryMailDomain: null
};
// Show login once everything is initialized
this.showLogin = false;
$timeout(function() { vm.showLogin = true; }, 100);
$timeout(function () {
vm.showLogin = true;
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
let token = urlParams.get('token');
if (0 < window.location.pathname.indexOf("passwordRecoveryEmail") && token) {
token = token.replace(/\//g, ''); // remove trailing '/'
const tokenArray = token.split(".");
// Retrieve info from token
if (3 === tokenArray.length) {
vm.passwordRecovery.passwordRecoveryToken = token;
const info = JSON.parse(atob(tokenArray[1]));
vm.creds.username = info.username;
vm.creds.domain = info.domain;
vm.passwordRecovery.passwordRecoveryToken = token;
vm.passwordRecovery.passwordRecoveryMode = "SecondaryEmail";
vm.passwordRecovery.passwordRecoveryEnabled = true;
vm.loginState = 'passwordchange';
vm.showLogin = false;
rippleDo('loginContent');
}
} else {
vm.retrievePasswordRecoveryEnabled();
}
}, 100);
};
this.login = function() {
@@ -98,7 +142,7 @@
vm.errorMessage = l('Your password is going to expire in %{0} %{1}.', value, string);
}
else if (msg.passwordexpired) {
vm.loginState = 'passwordexpired';
vm.loginState = 'passwordchange';
vm.url = msg.url;
}
@@ -109,6 +153,9 @@
this.restoreLogin = function() {
vm.loginState = false;
delete vm.creds.verificationCode;
if (vm.isInPasswordRecoveryMode()) {
vm.passwordRecoveryAbort();
}
};
this.continueLogin = function() {
@@ -138,6 +185,10 @@
$window.location.href = ApplicationBaseURL + 'login?language=' + this.creds.language;
};
this.hello = function (form) {
return !true;
}
this.canChangePassword = function(form) {
if (this.passwords.newPasswordConfirmation && this.passwords.newPasswordConfirmation.length &&
this.passwords.newPassword != this.passwords.newPasswordConfirmation) {
@@ -150,14 +201,15 @@
if (this.passwords.newPassword && this.passwords.newPassword.length > 0 &&
this.passwords.newPasswordConfirmation && this.passwords.newPasswordConfirmation.length &&
this.passwords.newPassword == this.passwords.newPasswordConfirmation &&
this.passwords.oldPassword && this.passwords.oldPassword.length > 0)
((this.isInPasswordRecoveryMode()) ||
(!this.loginState && this.passwords.oldPassword && this.passwords.oldPassword.length > 0)))
return true;
return false;
};
this.changePassword = function() {
Authentication.changePassword(this.creds.username, this.creds.domain, this.passwords.newPassword, this.passwords.oldPassword).then(function(data) {
Authentication.changePassword(this.creds.username, this.creds.domain, this.passwords.newPassword, this.passwords.oldPassword, this.passwordRecovery.passwordRecoveryToken).then(function(data) {
vm.loginState = 'message';
vm.url = data.url;
vm.errorMessage = l('The password was changed successfully.');
@@ -167,6 +219,95 @@
});
};
this.passwordRecoveryInfo = function () {
vm.loginState = 'passwordrecovery';
Authentication.passwordRecovery(this.creds.username, this.creds.domain).then(function (data) {
vm.passwordRecovery.passwordRecoveryMode = data.mode;
if ('SecretQuestion' === data.mode) {
vm.passwordRecovery.passwordRecoveryQuestion = data.secretQuestionLabel;
vm.passwordRecovery.passwordRecoveryQuestionKey = data.secretQuestion;
} else if ('SecondaryEmail' === data.mode) {
vm.passwordRecovery.passwordRecoverySecondaryEmailText = l("A link will be sent to %{0}", data.obfuscatedRecoveryEmail);
} else if ('Disabled' === data.mode) {
vm.loginState = 'error';
vm.errorMessage = l('No password recovery method has been defined for this user');
}
}, function (msg) {
vm.loginState = 'error';
vm.errorMessage = msg;
});
};
this.passwordRecoveryEmail = function () {
Authentication.passwordRecoveryEmail(this.creds.username, this.creds.domain
, this.passwordRecovery.passwordRecoveryMode
, this.passwordRecovery.passwordRecoveryMailDomain).then(function () {
vm.loginState = 'sendrecoverymail';
}, function (msg) {
vm.loginState = 'error';
vm.errorMessage = msg;
});
};
this.passwordRecoveryCheck = function () {
Authentication.passwordRecoveryCheck(this.creds.username, this.creds.domain
, this.passwordRecovery.passwordRecoveryMode
, this.passwordRecovery.passwordRecoveryQuestionKey
, this.passwordRecovery.passwordRecoveryQuestionAnswer
, this.passwordRecovery.passwordRecoveryMailDomain).then(function (token) {
if ("SecretQuestion" == vm.passwordRecovery.passwordRecoveryMode) {
vm.passwordRecovery.passwordRecoveryToken = token;
vm.loginState = 'passwordchange';
} else if ("SecondaryEmail" == vm.passwordRecovery.passwordRecoveryMode) {
vm.loginState = 'sendrecoverymail';
}
}, function (msg) {
vm.loginState = 'error';
vm.errorMessage = msg;
});
};
this.isInPasswordRecoveryMode = function () {
return (("SecretQuestion" == this.passwordRecovery.passwordRecoveryMode ||
"SecondaryEmail" == this.passwordRecovery.passwordRecoveryMode) &&
this.passwordRecovery.passwordRecoveryToken) ? true : false;
};
this.passwordRecoveryAbort = function () {
this.passwords = { newPassword: null, newPasswordConfirmation: null, oldPassword: null };
this.loginState = false;
this.passwordRecovery.passwordRecoveryEnabled = false;
this.passwordRecovery.passwordRecoveryQuestion = null;
this.passwordRecovery.passwordRecoveryMode = null;
this.passwordRecovery.passwordRecoveryQuestionAnswer = null;
this.passwordRecovery.passwordRecoveryToken = null;
this.passwordRecovery.passwordRecoverySecondaryEmailText = null;
this.passwordRecovery.passwordRecoveryMailDomain = null;
$window.location.href = vm.url;
};
this.usernameChanged = function () {
if (this.passwordRecovery.passwordRecoveryLinkTimer) {
clearTimeout(this.passwordRecovery.passwordRecoveryLinkTimer);
}
this.passwordRecovery.passwordRecoveryLinkTimer = setTimeout(() => {
vm.retrievePasswordRecoveryEnabled();
this.passwordRecovery.passwordRecoveryLinkTimer = null;
}, PASSWORD_RECOVERY_TIMER_MS);
};
this.retrievePasswordRecoveryEnabled = function () {
if (this.creds.username || this.creds.domain) {
Authentication.passwordRecoveryEnabled(this.creds.username, this.creds.domain).then(function (mailDomain) {
vm.passwordRecovery.passwordRecoveryMailDomain = mailDomain;
vm.passwordRecovery.passwordRecoveryEnabled = true;
}, function () {
vm.passwordRecovery.passwordRecoveryEnabled = false;
});
}
};
}
angular
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -393,6 +393,26 @@
return true;
};
/**
* @function isItemSecretQuestion
* @memberof Preferences.prototype
* @desc Check if the customer pick the secret question for e-mail recovery
* @returns true if secret question is selected
*/
Preferences.prototype.isItemSecretQuestion = function() {
return "SecretQuestion" == this.defaults.SOGoPasswordRecoveryMode;
};
/**
* @function isItemSecondaryEmail
* @memberof Preferences.prototype
* @desc Check if the customer pick the email for password recovery
* @returns true if email is selected
*/
Preferences.prototype.isItemSecondaryEmail = function () {
return "SecondaryEmail" == this.defaults.SOGoPasswordRecoveryMode;
};
/**
* @function authorizeNotifications
* @memberof Preferences.prototype
@@ -55,6 +55,7 @@
});
});
};
this.onDesktopNotificationsChange = function() {
if (this.preferences.defaults.SOGoDesktopNotifications)