mirror of
https://github.com/inverse-inc/sogo.git
synced 2026-05-23 04:15:26 +00:00
feat(password-recovery): Add password recovery with secret question or secondary email
This commit is contained in:
@@ -43,3 +43,6 @@ UI/WebServerResources/js/vendor/ckeditor/bender-runner.config.json
|
||||
UI/WebServerResources/js/vendor/ckeditor/plugins/onchange/docs
|
||||
UI/WebServerResources/js/vendor/ckeditor/plugins/scayt/*.md
|
||||
UI/WebServerResources/js/vendor/ckeditor/skins/n1theme/*.md
|
||||
|
||||
# JS
|
||||
UI/WebServerResources/js/*.map
|
||||
|
||||
@@ -786,6 +786,29 @@ Default value is `YES`, or enabled.
|
||||
authentication and global address books. Multiple sources can be
|
||||
specified as an array of dictionaries.
|
||||
|
||||
|S |SOGoPasswordRecovery
|
||||
|Boolean enable password recovery with secret question or secondary e-mail. Only for database user source.
|
||||
|
||||
|S |SOGoPasswordRecoveryDomains
|
||||
|List of domains where password recovery is enabled. If empty, enabled for all domains
|
||||
|
||||
|U |SOGoPasswordRecoveryMode
|
||||
|User password recovery mode. Values can be `Disabled`, `SecretQuestion` or `SecondaryEmail`.
|
||||
|
||||
|U |SOGoPasswordRecoveryQuestion
|
||||
|User password recovery secret question. Values can be `SecretQuestion1`, `SecretQuestion2` or `SecretQuestion3`.
|
||||
|
||||
|U |SOGoPasswordRecoveryQuestionAnswer
|
||||
|User password recovery secret question answer when mode is `SecretQuestion`.
|
||||
|
||||
|U |SOGoPasswordRecoverySecondaryEmail
|
||||
|User password recovery e-mail when mode is `SecondaryEmail`.
|
||||
|
||||
|S |SOGoJWTSecret
|
||||
|JWT secret according to RFC-7519. Default value is `SOGo`.
|
||||
|
||||
|
||||
|
||||
|=======================================================================
|
||||
|
||||
Authentication using LDAP
|
||||
|
||||
@@ -72,6 +72,7 @@ SOGo_HEADER_FILES = \
|
||||
SOGoDAVAuthenticator.h \
|
||||
SOGoProxyAuthenticator.h \
|
||||
SOGoStaticAuthenticator.h \
|
||||
SOGoEmptyAuthenticator.h \
|
||||
SOGoWebAuthenticator.h \
|
||||
SOGoWebDAVAclManager.h \
|
||||
SOGoWebDAVValue.h \
|
||||
@@ -88,7 +89,9 @@ SOGo_HEADER_FILES = \
|
||||
\
|
||||
SOGoCredentialsFile.h \
|
||||
SOGoTextTemplateFile.h \
|
||||
SOGoZipArchiver.h
|
||||
SOGoZipArchiver.h \
|
||||
\
|
||||
JWT.h
|
||||
|
||||
all::
|
||||
@touch SOGoBuild.m
|
||||
@@ -154,6 +157,7 @@ SOGo_OBJC_FILES = \
|
||||
SOGoDAVAuthenticator.m \
|
||||
SOGoProxyAuthenticator.m \
|
||||
SOGoStaticAuthenticator.m \
|
||||
SOGoEmptyAuthenticator.m \
|
||||
SOGoWebAuthenticator.m \
|
||||
SOGoWebDAVAclManager.m \
|
||||
SOGoWebDAVValue.m \
|
||||
@@ -170,7 +174,9 @@ SOGo_OBJC_FILES = \
|
||||
\
|
||||
SOGoCredentialsFile.m \
|
||||
SOGoTextTemplateFile.m \
|
||||
SOGoZipArchiver.m
|
||||
SOGoZipArchiver.m \
|
||||
\
|
||||
JWT.m
|
||||
|
||||
SOGo_C_FILES += lmhash.c aes.c crypt_blowfish.c pkcs5_pbkdf2.c
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
/* JWT.h - this file is part of SOGo
|
||||
*
|
||||
* Copyright (C) 2022 Alinto
|
||||
*
|
||||
* This file is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This file is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
* Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
#include <Foundation/NSString.h>
|
||||
|
||||
#ifndef JWT_H
|
||||
#define JWT_H
|
||||
|
||||
|
||||
@interface JWT : NSObject
|
||||
{
|
||||
@private
|
||||
NSString *JWTSecret;
|
||||
}
|
||||
|
||||
+ (JWT *)sharedInstance;
|
||||
- (NSString *) getJWTWithData: (NSDictionary *) data andValidity: (int)validitySec;
|
||||
- (NSDictionary *) getDataWithJWT: (NSString *) JWTToken andValidity: (BOOL *)isValid isExpired: (BOOL *)isExpired;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* JWT_H */
|
||||
@@ -0,0 +1,268 @@
|
||||
/* JWT.m - this file is part of SOGo
|
||||
*
|
||||
* Copyright (C) 2022 Alinto
|
||||
*
|
||||
* This file is part of SOGo.
|
||||
*
|
||||
* This file is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This file is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
* Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
#import "JWT.h"
|
||||
#import <Foundation/NSDictionary.h>
|
||||
#import <Foundation/NSData.h>
|
||||
#import <GNUstepBase/GSMime.h>
|
||||
#import <SOGo/SOGoSystemDefaults.h>
|
||||
#import <SOGo/NSDictionary+Utilities.h>
|
||||
#import <SOGo/NSString+Utilities.h>
|
||||
#include <openssl/hmac.h>
|
||||
#include <openssl/evp.h>
|
||||
|
||||
#define HS256_TOKEN_LENGH 43
|
||||
|
||||
static const NSString *kExpKey = @"exp";
|
||||
static const NSString *kAlgKey = @"alg";
|
||||
static const NSString *kTypKey = @"typ";
|
||||
static const NSString *kAlg = @"HS256";
|
||||
static const NSString *kTyp = @"JWT";
|
||||
|
||||
@implementation JWT
|
||||
|
||||
- (id) init
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
self->JWTSecret = [[SOGoSystemDefaults sharedSystemDefaults] JWTSecret];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) dealloc
|
||||
{
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
+ (JWT *)sharedInstance
|
||||
{
|
||||
static JWT *sharedInstance = nil;
|
||||
|
||||
if (!sharedInstance)
|
||||
{
|
||||
sharedInstance = [[self alloc] init];
|
||||
[sharedInstance retain];
|
||||
}
|
||||
|
||||
return sharedInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode base64 data
|
||||
* @param data The input data
|
||||
* @param length The input data length
|
||||
* @return Data encoded in base64
|
||||
*/
|
||||
- (NSString *) base64EncodeWithData: (NSData *)data length: (NSUInteger)length {
|
||||
NSData *dataBase64;
|
||||
dataBase64 = [GSMimeDocument encodeBase64: data];
|
||||
return [[
|
||||
[[NSString stringWithCString: [dataBase64 bytes] length: length]
|
||||
stringByReplacingOccurrencesOfString:@"+" withString:@"-"]
|
||||
stringByReplacingOccurrencesOfString:@"/" withString:@"_"]
|
||||
stringByReplacingOccurrencesOfString:@"=" withString:@""];
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode base64 string
|
||||
* @param data The input string
|
||||
* @param length The input data length
|
||||
* @return Data encoded in base64
|
||||
*/
|
||||
- (NSString *) base64EncodeWithString: (NSString *)data {
|
||||
return [[
|
||||
[[GSMimeDocument encodeBase64String: data]
|
||||
stringByReplacingOccurrencesOfString:@"+" withString:@"-"]
|
||||
stringByReplacingOccurrencesOfString:@"/" withString:@"_"]
|
||||
stringByReplacingOccurrencesOfString:@"=" withString:@""];
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode base64 string
|
||||
* @param data The base64 input string
|
||||
* @return Decoded data
|
||||
*/
|
||||
- (NSDictionary *) base64DecodeWithString: (NSString *)data {
|
||||
NSString *decodedData;
|
||||
NSDictionary *output;
|
||||
|
||||
output = nil;
|
||||
decodedData = [GSMimeDocument decodeBase64String: data];
|
||||
if ([decodedData isJSONString]) {
|
||||
output = (NSDictionary *)[decodedData objectFromJSONString];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate JWT Token encoded with HS256 algorithm
|
||||
* @param dict The payload content
|
||||
* @return A valid JWT token (header + payload + signature)
|
||||
*/
|
||||
- (NSString *) getHS256TokenForData: (NSDictionary *)dict withSecret: (NSString *)secret {
|
||||
unsigned char hs256[HS256_TOKEN_LENGH] = {};
|
||||
NSString *headerBase64, *payloadBase64, *content, *token;
|
||||
NSArray *sortedKeys;
|
||||
NSMutableDictionary *sortedDict
|
||||
|
||||
// Reorder dictionary keys
|
||||
sortedKeys = [[dict allKeys] sortedArrayUsingSelector: @selector(compare:)];
|
||||
sortedDict = [NSMutableDictionary dictionary];
|
||||
for (NSString *key in sortedKeys)
|
||||
[sortedDict setObject:[dict objectForKey: key] forKey: key];
|
||||
|
||||
headerBase64 = [self base64EncodeWithString:
|
||||
[[NSDictionary dictionaryWithObjectsAndKeys:kAlg, kAlgKey, kTyp, kTypKey, nil] jsonRepresentation]];
|
||||
payloadBase64 = [self base64EncodeWithString: [sortedDict jsonRepresentation]];
|
||||
content = [NSString stringWithFormat: @"%@.%@", headerBase64, payloadBase64, nil];
|
||||
|
||||
HMAC(EVP_sha256(),
|
||||
[secret UTF8String], [secret length],
|
||||
[content UTF8String], [content length],
|
||||
hs256, NULL);
|
||||
|
||||
token = [self base64EncodeWithData: [NSData dataWithBytes:hs256 length: HS256_TOKEN_LENGH] length: HS256_TOKEN_LENGH];
|
||||
|
||||
return [NSString stringWithFormat: @"%@.%@", content, token, nil];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate JWT Token encoded with HS256 algorithm
|
||||
* @param data The payload content
|
||||
* @param validitySec Validity duration, in seconds
|
||||
* @return A valid JWT token (header + payload + signature)
|
||||
*/
|
||||
- (NSString *)getJWTWithData: (NSDictionary *)data andValidity: (int)validitySec {
|
||||
NSMutableDictionary *dict;
|
||||
dict = [NSMutableDictionary dictionaryWithDictionary: data];
|
||||
[dict setObject:[NSString stringWithFormat:@"%.0f", ([[NSDate date] timeIntervalSince1970] + validitySec)] forKey: kExpKey];
|
||||
|
||||
return [self getHS256TokenForData: dict withSecret: self->JWTSecret];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JWT token data and check validity token
|
||||
* @param JWTToken A JWT complete token (header + payload + signature)
|
||||
* @param isValid Reference parameter - NO if token is invalid
|
||||
* @param isExpired Reference parameter - YES if token is expired
|
||||
* @return Payload content
|
||||
*/
|
||||
- (NSDictionary *)getDataWithJWT: (NSString *)JWTToken andValidity: (BOOL *)isValid isExpired: (BOOL *)isExpired {
|
||||
NSArray *components, *reencodedComponents;
|
||||
NSString *header, *payload, *reencodedJWTToken, *signature, *reencodedSignature;
|
||||
NSDictionary *headerDict, *payloadDict;
|
||||
NSTimeInterval tokenTime;
|
||||
NSMutableDictionary *result;
|
||||
|
||||
*isValid = YES;
|
||||
*isExpired = NO;
|
||||
result = nil;
|
||||
components = [JWTToken componentsSeparatedByString:@"."];
|
||||
|
||||
if (3 != [components count]) {
|
||||
// Invalid number of components
|
||||
*isValid = NO;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check header
|
||||
///////////////
|
||||
header = (NSString *)[components objectAtIndex: 0];
|
||||
if (!header) {
|
||||
// No header
|
||||
*isValid = NO;
|
||||
return result;
|
||||
}
|
||||
headerDict = [self base64DecodeWithString: header];
|
||||
if (!headerDict) {
|
||||
// No header
|
||||
*isValid = NO;
|
||||
return result;
|
||||
}
|
||||
if (![headerDict objectForKey: kTypKey] || ![[headerDict objectForKey: kTypKey] isEqualToString: kTyp]) {
|
||||
// Invalid type
|
||||
*isValid = NO;
|
||||
return result;
|
||||
}
|
||||
if (![headerDict objectForKey: kAlgKey] || ![[headerDict objectForKey: kAlgKey] isEqualToString: kAlg]) {
|
||||
// Invalid algorithm
|
||||
*isValid = NO;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check payload
|
||||
///////////////
|
||||
payload = (NSString *)[components objectAtIndex: 1];
|
||||
if (!payload) {
|
||||
// No payload
|
||||
*isValid = NO;
|
||||
return result;
|
||||
}
|
||||
payloadDict = [self base64DecodeWithString: payload];
|
||||
if (!payloadDict) {
|
||||
// No payload
|
||||
*isValid = NO;
|
||||
return result;
|
||||
}
|
||||
if (![payloadDict objectForKey: kExpKey]) {
|
||||
// No expiration token
|
||||
*isValid = NO;
|
||||
return result;
|
||||
}
|
||||
// Check expiration
|
||||
tokenTime = [[payloadDict objectForKey: kExpKey] doubleValue];
|
||||
if (0 != tokenTime) { // 0 for infinity validation
|
||||
if ([[NSDate date] timeIntervalSince1970] > tokenTime) {
|
||||
// Token expired
|
||||
*isValid = NO;
|
||||
*isExpired = YES;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// Check signature
|
||||
///////////////
|
||||
reencodedJWTToken = [self getHS256TokenForData: payloadDict withSecret: self->JWTSecret];
|
||||
reencodedComponents = [reencodedJWTToken componentsSeparatedByString:@"."];
|
||||
if (3 != [reencodedComponents count]) {
|
||||
// Invalid number of reencoded components
|
||||
*isValid = NO;
|
||||
return result;
|
||||
}
|
||||
signature = (NSString *)[components objectAtIndex: 2];
|
||||
reencodedSignature = (NSString *)[reencodedComponents objectAtIndex: 2];
|
||||
if (![signature isEqualToString: reencodedSignature]) {
|
||||
// Invalid signature
|
||||
*isValid = NO;
|
||||
return result;
|
||||
}
|
||||
|
||||
// All is OK !
|
||||
result = [NSMutableDictionary dictionaryWithDictionary: payloadDict];
|
||||
[result removeObjectForKey: kExpKey];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -708,14 +708,20 @@ groupObjectClasses: (NSArray *) newGroupObjectClasses
|
||||
return didChange;
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
//
|
||||
/**
|
||||
* Change a user's password.
|
||||
* @param login the user's login name.
|
||||
* @param oldPassword the previous password.
|
||||
* @param newPassword the new password.
|
||||
* @param perr will be set if the new password is not conform to the policy.
|
||||
* @param passwordRecovery YES of this is password recovery, NO otherwise. If password recovery is set, old password won't be checked
|
||||
* @return YES if the password was successfully changed.
|
||||
*/
|
||||
- (BOOL) changePasswordForLogin: (NSString *) login
|
||||
oldPassword: (NSString *) oldPassword
|
||||
newPassword: (NSString *) newPassword
|
||||
passwordRecovery: (BOOL) passwordRecovery
|
||||
perr: (SOGoPasswordPolicyError *) perr
|
||||
|
||||
{
|
||||
NGLdapConnection *bindConnection;
|
||||
NSString *userDN;
|
||||
|
||||
@@ -58,12 +58,12 @@
|
||||
while ((currentKey = [keys nextObject]))
|
||||
{
|
||||
currentValue = [[self objectForKey: currentKey] jsonRepresentation];
|
||||
currentPair = [NSString stringWithFormat: @"%@: %@",
|
||||
currentPair = [NSString stringWithFormat: @"%@:%@",
|
||||
[currentKey jsonRepresentation], currentValue];
|
||||
[values addObject: currentPair];
|
||||
}
|
||||
representation = [NSString stringWithFormat: @"{%@}",
|
||||
[values componentsJoinedByString: @", "]];
|
||||
[values componentsJoinedByString: @","]];
|
||||
|
||||
return representation;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,8 @@ typedef enum
|
||||
PolicyPasswordTooShort = 6,
|
||||
PolicyPasswordTooYoung = 7,
|
||||
PolicyPasswordInHistory = 8,
|
||||
PolicyPasswordRecoveryFailed = 9,
|
||||
PolicyPasswordRecoveryInvalidToken = 10,
|
||||
PolicyNoError = 65535,
|
||||
} SOGoPasswordPolicyError;
|
||||
|
||||
|
||||
@@ -195,4 +195,7 @@
|
||||
};
|
||||
|
||||
SOGoSubscriptionFolderFormat = "%{FolderName} (%{UserName} <%{Email}>)";
|
||||
|
||||
SOGoPasswordRecoveryMode = "Disabled";
|
||||
SOGoPasswordRecoveryQuestion = "SecretQuestion1";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/* SOGoEmptyAuthenticator.h - this file is part of SOGo
|
||||
*
|
||||
* Copyright (C) 2022 Alinto
|
||||
*
|
||||
* This file is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This file is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
* Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
#ifndef SOGOEMPTYAUTHENTICATOR_H
|
||||
#define SOGOEMPTYAUTHENTICATOR_H
|
||||
|
||||
|
||||
|
||||
#import "SOGoAuthenticator.h"
|
||||
|
||||
/*
|
||||
SOGoEmptyAuthenticator
|
||||
This class can be used to send message without SMTP authentication.
|
||||
This is used to send technical message to users, e.g. for password recovery
|
||||
|
||||
*/
|
||||
|
||||
@interface SOGoEmptyAuthenticator : NSObject <SOGoAuthenticator>
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
+ (id) sharedSOGoEmptyAuthenticator;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SOGOEMPTYAUTHENTICATOR_H */
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/* SOGoEmptyAuthenticator.m - this file is part of SOGo
|
||||
*
|
||||
* Copyright (C) 2022 Alinto
|
||||
*
|
||||
* This file is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This file is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||
* Boston, MA 02111-1307, USA.
|
||||
*/
|
||||
|
||||
#import <Foundation/NSString.h>
|
||||
|
||||
#import "SOGoEmptyAuthenticator.h"
|
||||
|
||||
@implementation SOGoEmptyAuthenticator
|
||||
|
||||
+ (id) sharedSOGoEmptyAuthenticator
|
||||
{
|
||||
static SOGoEmptyAuthenticator *auth = nil;
|
||||
|
||||
if (!auth)
|
||||
auth = [self new];
|
||||
|
||||
return auth;
|
||||
}
|
||||
|
||||
- (id) init
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) dealloc
|
||||
{
|
||||
[super dealloc];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -30,10 +30,10 @@
|
||||
|
||||
#import "NSString+Utilities.h"
|
||||
#import "SOGoStaticAuthenticator.h"
|
||||
#import "SOGoEmptyAuthenticator.h"
|
||||
#import "SOGoSystemDefaults.h"
|
||||
#import "SOGoUser.h"
|
||||
#import "SOGoUserManager.h"
|
||||
|
||||
#import "SOGoMailer.h"
|
||||
|
||||
//
|
||||
@@ -243,7 +243,7 @@
|
||||
NS_DURING
|
||||
{
|
||||
[client connect];
|
||||
if ([authenticationType isEqualToString: @"plain"])
|
||||
if ([authenticationType isEqualToString: @"plain"] && ![authenticator isKindOfClass: [SOGoEmptyAuthenticator class]])
|
||||
{
|
||||
/* XXX Allow static credentials by peeking at the classname */
|
||||
if ([authenticator isKindOfClass: [SOGoStaticAuthenticator class]])
|
||||
@@ -262,7 +262,7 @@
|
||||
reason: @"cannot send message:"
|
||||
@" (smtp) authentication failure"];
|
||||
}
|
||||
else if (authenticationType)
|
||||
else if (authenticationType && ![authenticator isKindOfClass: [SOGoEmptyAuthenticator class]])
|
||||
result = [NSException
|
||||
exceptionWithHTTPStatus: 500
|
||||
reason: @"cannot send message:"
|
||||
|
||||
@@ -57,9 +57,10 @@
|
||||
grace: (int *) _grace;
|
||||
|
||||
- (BOOL) changePasswordForLogin: (NSString *) login
|
||||
oldPassword: (NSString *) oldPassword
|
||||
newPassword: (NSString *) newPassword
|
||||
perr: (SOGoPasswordPolicyError *) perr;
|
||||
oldPassword: (NSString *) oldPassword
|
||||
newPassword: (NSString *) newPassword
|
||||
passwordRecovery: (BOOL) passwordRecovery
|
||||
perr: (SOGoPasswordPolicyError *) perr;
|
||||
|
||||
- (NSDictionary *) lookupContactEntry: (NSString *) theID
|
||||
inDomain: (NSString *) domain;
|
||||
|
||||
@@ -115,6 +115,10 @@
|
||||
- (int) maximumPictureSize;
|
||||
- (BOOL) easSearchInBody;
|
||||
|
||||
- (BOOL) isPasswordRecoveryEnabled;
|
||||
- (NSArray *) passwordRecoveryDomains;
|
||||
- (NSString *) JWTSecret;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SOGOSYSTEMDEFAULTS_H */
|
||||
|
||||
@@ -755,4 +755,34 @@ _injectConfigurationFromFile (NSMutableDictionary *defaultsDict,
|
||||
return v;
|
||||
}
|
||||
|
||||
- (BOOL) isPasswordRecoveryEnabled
|
||||
{
|
||||
return [self boolForKey: @"SOGoPasswordRecovery"];
|
||||
}
|
||||
|
||||
- (NSArray *) passwordRecoveryDomains
|
||||
{
|
||||
static NSArray *passwordRecoveryDomains = nil;
|
||||
|
||||
if (!passwordRecoveryDomains)
|
||||
{
|
||||
passwordRecoveryDomains = [self stringArrayForKey: @"SOGoPasswordRecoveryDomains"];
|
||||
[passwordRecoveryDomains retain];
|
||||
}
|
||||
|
||||
return passwordRecoveryDomains;
|
||||
}
|
||||
|
||||
- (NSString *) JWTSecret
|
||||
{
|
||||
NSString *secret;
|
||||
|
||||
secret = [self stringForKey: @"SOGoJWTSecret"];
|
||||
|
||||
if (!secret)
|
||||
secret = @"SOGo"; // Default secret
|
||||
|
||||
return secret;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -33,6 +33,14 @@ extern NSString *SOGoWeekStartJanuary1;
|
||||
extern NSString *SOGoWeekStartFirst4DayWeek;
|
||||
extern NSString *SOGoWeekStartFirstFullWeek;
|
||||
|
||||
extern NSString *SOGoPasswordRecoveryDisabled;
|
||||
extern NSString *SOGoPasswordRecoveryQuestion;
|
||||
extern NSString *SOGoPasswordRecoveryQuestion1;
|
||||
extern NSString *SOGoPasswordRecoveryQuestion2;
|
||||
extern NSString *SOGoPasswordRecoveryQuestion3;
|
||||
extern NSString *SOGoPasswordRecoverySecondaryEmail;
|
||||
|
||||
|
||||
@interface SOGoUserDefaults : SOGoDefaultsSource
|
||||
{
|
||||
NSString *userLanguage;
|
||||
@@ -236,6 +244,16 @@ extern NSString *SOGoWeekStartFirstFullWeek;
|
||||
- (void) setContactsCategories: (NSArray *) newValues;
|
||||
- (NSArray *) contactsCategories;
|
||||
|
||||
/* Password recovery */
|
||||
- (void) setPasswordRecoveryMode: (NSString *) newValue;
|
||||
- (NSString *) passwordRecoveryMode;
|
||||
- (void) setPasswordRecoveryQuestion: (NSString *) newValue;
|
||||
- (NSString *) passwordRecoveryQuestion;
|
||||
- (void) setPasswordRecoveryQuestionAnswer: (NSString *) newValue;
|
||||
- (NSString *) passwordRecoveryQuestionAnswer;
|
||||
- (void) setPasswordRecoverySecondaryEmail: (NSString *) newValue;
|
||||
- (NSString *) passwordRecoverySecondaryEmail;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* SOGOUSERDEFAULTS_H */
|
||||
|
||||
@@ -39,6 +39,14 @@ NSString *SOGoWeekStartJanuary1 = @"January1";
|
||||
NSString *SOGoWeekStartFirst4DayWeek = @"First4DayWeek";
|
||||
NSString *SOGoWeekStartFirstFullWeek = @"FirstFullWeek";
|
||||
|
||||
NSString *SOGoPasswordRecoveryDisabled = @"Disabled";
|
||||
NSString *SOGoPasswordRecoveryQuestion = @"SecretQuestion";
|
||||
NSString *SOGoPasswordRecoveryQuestion1 = @"SecretQuestion1";
|
||||
NSString *SOGoPasswordRecoveryQuestion2 = @"SecretQuestion2";
|
||||
NSString *SOGoPasswordRecoveryQuestion3 = @"SecretQuestion3";
|
||||
NSString *SOGoPasswordRecoverySecondaryEmail = @"SecondaryEmail";
|
||||
|
||||
|
||||
@implementation SOGoUserDefaults
|
||||
|
||||
+ (NSString *) userProfileClassName
|
||||
@@ -941,4 +949,44 @@ NSString *SOGoWeekStartFirstFullWeek = @"FirstFullWeek";
|
||||
return [self stringArrayForKey: @"SOGoContactsCategories"];
|
||||
}
|
||||
|
||||
- (void) setPasswordRecoveryMode: (NSString *) newValue
|
||||
{
|
||||
[self setObject: newValue forKey: @"SOGoPasswordRecoveryMode"];
|
||||
}
|
||||
|
||||
- (NSString *) passwordRecoveryMode
|
||||
{
|
||||
return [self stringForKey: @"SOGoPasswordRecoveryMode"];
|
||||
}
|
||||
|
||||
- (void) setPasswordRecoveryQuestion: (NSString *) newValue
|
||||
{
|
||||
[self setObject: newValue forKey: @"SOGoPasswordRecoveryQuestion"];
|
||||
}
|
||||
|
||||
- (NSString *) passwordRecoveryQuestion
|
||||
{
|
||||
return [self stringForKey: @"SOGoPasswordRecoveryQuestion"];
|
||||
}
|
||||
|
||||
- (void) setPasswordRecoveryQuestionAnswer: (NSString *) newValue
|
||||
{
|
||||
[self setObject: newValue forKey: @"SOGoPasswordRecoveryQuestionAnswer"];
|
||||
}
|
||||
|
||||
- (NSString *) passwordRecoveryQuestionAnswer
|
||||
{
|
||||
return [self stringForKey: @"SOGoPasswordRecoveryQuestionAnswer"];
|
||||
}
|
||||
|
||||
- (void) setPasswordRecoverySecondaryEmail: (NSString *) newValue
|
||||
{
|
||||
[self setObject: newValue forKey: @"SOGoPasswordRecoverySecondaryEmail"];
|
||||
}
|
||||
|
||||
- (NSString *) passwordRecoverySecondaryEmail
|
||||
{
|
||||
return [self stringForKey: @"SOGoPasswordRecoverySecondaryEmail"];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -99,9 +99,16 @@
|
||||
|
||||
- (BOOL) changePasswordForLogin: (NSString *) login
|
||||
inDomain: (NSString *) domain
|
||||
oldPassword: (NSString *) oldPassword
|
||||
newPassword: (NSString *) newPassword
|
||||
perr: (SOGoPasswordPolicyError *) perr;
|
||||
oldPassword: (NSString *) oldPassword
|
||||
newPassword: (NSString *) newPassword
|
||||
passwordRecovery: (BOOL) passwordRecovery
|
||||
token: (NSString *) token
|
||||
perr: (SOGoPasswordPolicyError *) perr;
|
||||
- (NSDictionary *) getPasswordRecoveryInfosForUsername: (NSString *) username domain:(NSString *) domain;
|
||||
- (NSString *) generateAndSavePasswordRecoveryTokenWithUid: (NSString *) uid username: (NSString *) username domain:(NSString *) domain;
|
||||
- (NSString *) getTokenAndCheckPasswordRecoveryDataForUsername: (NSString *) username domain:(NSString *) domain withData:(NSDictionary *) passwordRecoveryData;
|
||||
- (NSString *) getPasswordRecoveryTokenFor: (NSString *) username domain:(NSString *) domain;
|
||||
- (BOOL) isPasswordRecoveryTokenValidFor: (NSString *) token uid: (NSString *) uid;
|
||||
@end
|
||||
|
||||
#endif /* SOGOUSERMANAGER_H */
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#import <NGExtensions/NSNull+misc.h>
|
||||
#import <NGExtensions/NSObject+Logs.h>
|
||||
|
||||
#import <NGObjWeb/WOApplication.h>
|
||||
|
||||
#import "NSArray+Utilities.h"
|
||||
#import "NSString+Utilities.h"
|
||||
#import "NSString+Crypto.h"
|
||||
@@ -32,9 +34,23 @@
|
||||
#import "SOGoUserManager.h"
|
||||
#import "SOGoCache.h"
|
||||
#import "SOGoSource.h"
|
||||
#import "SOGoUserDefaults.h"
|
||||
#import "SOGoUserSettings.h"
|
||||
#import "WOResourceManager+SOGo.h"
|
||||
#import "JWT.h"
|
||||
|
||||
static Class NSNullK;
|
||||
|
||||
static const int kJwtDurationS = 600; // 10 minutes
|
||||
static const NSString *kUidKey = @"uid";
|
||||
static const NSString *kUsernameKey = @"username";
|
||||
static const NSString *kDomainKey = @"domain";
|
||||
static const NSString *kPasswordRecoveryTokenKey = @"passwordRecoveryToken";
|
||||
static const NSString *kModeKey = @"mode";
|
||||
static const NSString *kSecretQuestionKey = @"secretQuestion";
|
||||
static const NSString *kObfuscatedSecondaryEmailKey = @"obfuscatedSecondaryEmail";
|
||||
|
||||
|
||||
@implementation SOGoUserManagerRegistry
|
||||
|
||||
+ (void) initialize
|
||||
@@ -453,7 +469,8 @@ static Class NSNullK;
|
||||
inDomain: (NSString *) domain
|
||||
oldPassword: (NSString *) oldPassword
|
||||
newPassword: (NSString *) newPassword
|
||||
perr: (SOGoPasswordPolicyError *) perr
|
||||
passwordRecovery: (BOOL) passwordRecovery
|
||||
perr: (SOGoPasswordPolicyError *) perr
|
||||
{
|
||||
NSObject <SOGoSource> *sogoSource;
|
||||
NSString *currentID;
|
||||
@@ -472,6 +489,7 @@ static Class NSNullK;
|
||||
didChange = [sogoSource changePasswordForLogin: login
|
||||
oldPassword: oldPassword
|
||||
newPassword: newPassword
|
||||
passwordRecovery: passwordRecovery
|
||||
perr: perr];
|
||||
}
|
||||
|
||||
@@ -740,25 +758,45 @@ static Class NSNullK;
|
||||
inDomain: (NSString *) domain
|
||||
oldPassword: (NSString *) oldPassword
|
||||
newPassword: (NSString *) newPassword
|
||||
passwordRecovery: (BOOL) passwordRecovery
|
||||
token: (NSString *) token
|
||||
perr: (SOGoPasswordPolicyError *) perr
|
||||
{
|
||||
NSString *jsonUser, *userLogin;
|
||||
NSString *jsonUser, *userLogin, *uid, *userToken;
|
||||
NSMutableDictionary *currentUser;
|
||||
BOOL didChange;
|
||||
SOGoSystemDefaults *sd;
|
||||
NSDictionary *info;
|
||||
SOGoUserSettings *us;
|
||||
|
||||
jsonUser = [[SOGoCache sharedCache] userAttributesForLogin: login];
|
||||
currentUser = [jsonUser objectFromJSONString];
|
||||
|
||||
if ([currentUser isKindOfClass: NSNullK])
|
||||
currentUser = nil;
|
||||
|
||||
// Check session id for password recovery
|
||||
userToken = [self getPasswordRecoveryTokenFor: login domain: domain];
|
||||
info = [self contactInfosForUserWithUIDorEmail: login];
|
||||
uid = [info objectForKey: @"c_uid"];
|
||||
|
||||
if ([self _sourceChangePasswordForLogin: login
|
||||
if (passwordRecovery && (![userToken isEqualToString: token] || ![self isPasswordRecoveryTokenValidFor: userToken uid: uid])) {
|
||||
didChange = NO;
|
||||
*perr = PolicyPasswordRecoveryInvalidToken;
|
||||
} else if ([self _sourceChangePasswordForLogin: login
|
||||
inDomain: domain
|
||||
oldPassword: oldPassword
|
||||
newPassword: newPassword
|
||||
passwordRecovery: passwordRecovery
|
||||
perr: perr])
|
||||
{
|
||||
if (passwordRecovery) {
|
||||
// Reset session id
|
||||
us = [SOGoUserSettings settingsForUser: uid];
|
||||
[us removeObjectForKey: kPasswordRecoveryTokenKey];
|
||||
[us synchronize];
|
||||
}
|
||||
|
||||
didChange = YES;
|
||||
|
||||
if (!currentUser)
|
||||
@@ -1283,4 +1321,179 @@ static Class NSNullK;
|
||||
return login;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get password reovery info for specific user (mode, secret question, obfuscated recovery email)
|
||||
* @param username Username
|
||||
* @param domain Domain
|
||||
* @return User informations
|
||||
*/
|
||||
- (NSDictionary *) getPasswordRecoveryInfosForUsername: (NSString *) username domain:(NSString *) domain
|
||||
{
|
||||
SOGoUserDefaults *userDefaults;
|
||||
NSDictionary *info, *data;
|
||||
SOGoSystemDefaults *sd;
|
||||
NSString *uid, *suffix;
|
||||
NSMutableString *obfuscatedSecondaryEmail;
|
||||
NSInteger secondaryEmailLength = 0, obfuscatedSecondaryEmailLength = 0, i = 0;
|
||||
|
||||
userDefaults = nil;
|
||||
info = [self contactInfosForUserWithUIDorEmail: username];
|
||||
uid = [info objectForKey: @"c_uid"];
|
||||
|
||||
sd = [SOGoSystemDefaults sharedSystemDefaults];
|
||||
if ([sd enableDomainBasedUID]
|
||||
&& ![[info objectForKey: @"DomainLessLogin"] boolValue])
|
||||
{
|
||||
suffix = [NSString stringWithFormat: @"@%@", domain];
|
||||
|
||||
// Don't add @domain suffix if it's already there
|
||||
if (![uid hasSuffix: suffix])
|
||||
uid = [NSString stringWithFormat: @"%@%@", uid, suffix];
|
||||
|
||||
userDefaults = [SOGoUserDefaults defaultsForUser: uid
|
||||
inDomain: domain];
|
||||
} else {
|
||||
userDefaults = [SOGoUserDefaults defaultsForUser: uid
|
||||
inDomain: nil];
|
||||
}
|
||||
|
||||
if (nil != userDefaults && [[userDefaults passwordRecoveryMode] isEqualToString: SOGoPasswordRecoveryQuestion]) {
|
||||
data = [NSDictionary dictionaryWithObjectsAndKeys: [userDefaults passwordRecoveryMode], kModeKey,
|
||||
[userDefaults passwordRecoveryQuestion], kSecretQuestionKey,
|
||||
nil];
|
||||
} else if (nil != userDefaults && [[userDefaults passwordRecoveryMode] isEqualToString: SOGoPasswordRecoverySecondaryEmail]) {
|
||||
secondaryEmailLength = [[userDefaults passwordRecoverySecondaryEmail] length];
|
||||
// Obfuscate 80% of email. Compute nb character obfuscated
|
||||
obfuscatedSecondaryEmailLength = (NSUInteger)(0.8 * secondaryEmailLength);
|
||||
obfuscatedSecondaryEmail = [NSMutableString stringWithString: @"*"];
|
||||
for (i = 1 ; i < secondaryEmailLength ; i++) {
|
||||
if (i < obfuscatedSecondaryEmailLength
|
||||
&& ![[[userDefaults passwordRecoverySecondaryEmail] substringWithRange:NSMakeRange(i, 1)] isEqualToString: @"@"]) {
|
||||
[obfuscatedSecondaryEmail appendString: @"*"];
|
||||
} else {
|
||||
[obfuscatedSecondaryEmail appendString: [[userDefaults passwordRecoverySecondaryEmail] substringWithRange:NSMakeRange(i, 1)]];
|
||||
}
|
||||
}
|
||||
|
||||
data = [NSDictionary dictionaryWithObjectsAndKeys: [userDefaults passwordRecoveryMode], kModeKey, obfuscatedSecondaryEmail, kObfuscatedSecondaryEmailKey, nil];
|
||||
} else {
|
||||
data = [NSDictionary dictionaryWithObject: SOGoPasswordRecoveryDisabled forKey: kModeKey];
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* (Email recovery) Generates and save in user pref the JWT token
|
||||
* @param uid User id
|
||||
* @param username Username
|
||||
* @param domain Domain
|
||||
* @return JWT Token
|
||||
*/
|
||||
- (NSString *) generateAndSavePasswordRecoveryTokenWithUid: (NSString *) uid username: (NSString *) username domain:(NSString *) domain
|
||||
{
|
||||
NSString *jwtToken;
|
||||
SOGoUserSettings *us;
|
||||
|
||||
us = [SOGoUserSettings settingsForUser: uid];
|
||||
jwtToken = [[JWT sharedInstance] getJWTWithData:
|
||||
[NSDictionary dictionaryWithObjectsAndKeys: uid, kUidKey, username, kUsernameKey, domain, kDomainKey, nil]
|
||||
andValidity: kJwtDurationS];
|
||||
|
||||
[us setObject: jwtToken forKey: kPasswordRecoveryTokenKey];
|
||||
[us synchronize];
|
||||
|
||||
return jwtToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* (Secret Question) Check secret question answer, generates and save in user pref the JWT token
|
||||
* @param username Username
|
||||
* @param domain Domain
|
||||
* @param passwordRecoveryData Password recovery data (answer, ...)
|
||||
* @return JWT Token
|
||||
*/
|
||||
- (NSString *) getTokenAndCheckPasswordRecoveryDataForUsername: (NSString *) username domain:(NSString *) domain withData:(NSDictionary *) passwordRecoveryData
|
||||
{
|
||||
NSString *mode, *questionKey, *answer, *uid, *suffix, *token;
|
||||
SOGoUserDefaults *userDefaults;
|
||||
SOGoSystemDefaults *sd;
|
||||
NSDictionary *info;
|
||||
|
||||
mode = [passwordRecoveryData objectForKey:@"mode"];
|
||||
questionKey = [passwordRecoveryData objectForKey:@"question"];
|
||||
answer = [[[passwordRecoveryData objectForKey:@"answer"] lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||
|
||||
token = nil;
|
||||
userDefaults = nil;
|
||||
info = [self contactInfosForUserWithUIDorEmail: username];
|
||||
uid = [info objectForKey: @"c_uid"];
|
||||
sd = [SOGoSystemDefaults sharedSystemDefaults];
|
||||
if ([sd enableDomainBasedUID]
|
||||
&& ![[info objectForKey: @"DomainLessLogin"] boolValue])
|
||||
{
|
||||
suffix = [NSString stringWithFormat: @"@%@", domain];
|
||||
|
||||
// Don't add @domain suffix if it's already there
|
||||
if (![uid hasSuffix: suffix])
|
||||
uid = [NSString stringWithFormat: @"%@%@", uid, suffix];
|
||||
|
||||
userDefaults = [SOGoUserDefaults defaultsForUser: uid
|
||||
inDomain: domain];
|
||||
} else {
|
||||
userDefaults = [SOGoUserDefaults defaultsForUser: uid
|
||||
inDomain: nil];
|
||||
}
|
||||
|
||||
if ([sd isPasswordRecoveryEnabled] && nil != userDefaults && [[userDefaults passwordRecoveryMode] isEqualToString: mode]) {
|
||||
if ([[userDefaults passwordRecoveryQuestion] isEqualToString: questionKey]
|
||||
&& [[[[userDefaults passwordRecoveryQuestionAnswer] lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] isEqualToString: answer]) {
|
||||
token = [self generateAndSavePasswordRecoveryTokenWithUid: uid username: username domain: domain];
|
||||
}
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token for username / domain stored in user pref
|
||||
* @param username Username
|
||||
* @param domain Domain
|
||||
* @return JWT Token
|
||||
*/
|
||||
- (NSString *) getPasswordRecoveryTokenFor: (NSString *) username domain: (NSString *) domain
|
||||
{
|
||||
NSString *uid;
|
||||
NSDictionary *info;
|
||||
SOGoUserSettings *us;
|
||||
|
||||
info = [self contactInfosForUserWithUIDorEmail: username];
|
||||
uid = [info objectForKey: @"c_uid"];
|
||||
us = [SOGoUserSettings settingsForUser: uid];
|
||||
[us synchronize];
|
||||
|
||||
return [us stringForKey: kPasswordRecoveryTokenKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if JWT token is valid
|
||||
* @param token Username
|
||||
* @param uid User id
|
||||
* @return True or False
|
||||
*/
|
||||
- (BOOL) isPasswordRecoveryTokenValidFor: (NSString *) token uid: (NSString *) uid {
|
||||
BOOL isJWTValid, isJWTExpired, r;
|
||||
NSDictionary *results;
|
||||
|
||||
r = NO;
|
||||
results = [[JWT sharedInstance] getDataWithJWT: token andValidity: &isJWTValid isExpired: &isJWTExpired];
|
||||
if (isJWTValid && !isJWTExpired
|
||||
&& [results objectForKey: kUidKey]
|
||||
&& [[results objectForKey: kUidKey] isEqualToString: uid]) {
|
||||
r = YES;
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -317,7 +317,9 @@
|
||||
row = [channel fetchAttributes: attrs withZone: NULL];
|
||||
value = [row objectForKey: @"c_password"];
|
||||
|
||||
rc = [self _isPassword: _pwd equalTo: value];
|
||||
if (_pwd != (id)[NSNull null]) {
|
||||
rc = [self _isPassword: _pwd equalTo: value];
|
||||
}
|
||||
[channel cancelFetch];
|
||||
}
|
||||
else
|
||||
@@ -338,11 +340,13 @@
|
||||
* @param oldPassword the previous password.
|
||||
* @param newPassword the new password.
|
||||
* @param perr will be set if the new password is not conform to the policy.
|
||||
* @param passwordRecovery YES of this is password recovery, NO otherwise. If password recovery is set, old password won't be checked
|
||||
* @return YES if the password was successfully changed.
|
||||
*/
|
||||
- (BOOL) changePasswordForLogin: (NSString *) login
|
||||
oldPassword: (NSString *) oldPassword
|
||||
newPassword: (NSString *) newPassword
|
||||
passwordRecovery: (BOOL) passwordRecovery
|
||||
perr: (SOGoPasswordPolicyError *) perr
|
||||
{
|
||||
BOOL didChange, isOldPwdOk, isPolicyOk;
|
||||
@@ -362,7 +366,7 @@
|
||||
// Verify current password
|
||||
isOldPwdOk = [self checkLogin:login password:oldPassword perr:perr expire:0 grace:0];
|
||||
|
||||
if (isOldPwdOk)
|
||||
if (isOldPwdOk || passwordRecovery)
|
||||
{
|
||||
if ([_userPasswordPolicy count])
|
||||
{
|
||||
@@ -386,7 +390,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (isOldPwdOk && isPolicyOk)
|
||||
if ((isOldPwdOk || passwordRecovery) && isPolicyOk)
|
||||
{
|
||||
// Encrypt new password
|
||||
NSString *encryptedPassword = [self _encryptPassword: newPassword];
|
||||
|
||||
@@ -222,3 +222,11 @@
|
||||
|
||||
/* Hotkey description to move forward in current view */
|
||||
"Move forward" = "Move forward";
|
||||
|
||||
/* Password Recovery */
|
||||
"passwordRecovery_Disabled" = "Disabled";
|
||||
"passwordRecovery_SecretQuestion" = "Secret question";
|
||||
"passwordRecovery_SecretQuestion1" = "What is the name of your first pet ?";
|
||||
"passwordRecovery_SecretQuestion2" = "What was your first car ?";
|
||||
"passwordRecovery_SecretQuestion3" = "What is your favorite movie ?";
|
||||
"passwordRecovery_SecondaryEmail" = "Secondary E-mail";
|
||||
|
||||
@@ -222,3 +222,12 @@
|
||||
|
||||
/* Hotkey description to move forward in current view */
|
||||
"Move forward" = "Reculer";
|
||||
|
||||
/* Password Recovery */
|
||||
"passwordRecovery_Disabled" = "Désactivée";
|
||||
"passwordRecovery_SecretQuestion" = "Question secrète";
|
||||
"passwordRecovery_SecretQuestion1" = "Quel est le nom de votre premier animal de compagnie ?";
|
||||
"passwordRecovery_SecretQuestion2" = "Quelle était votre première voiture ?";
|
||||
"passwordRecovery_SecretQuestion3" = "Quel est votre film préféré ?";
|
||||
"passwordRecovery_SecondaryEmail" = "E-mail secondaire";
|
||||
|
||||
|
||||
@@ -113,3 +113,19 @@ See <a href=\"http://www.sogo.nu/en/support/community.html\">this page</a> for v
|
||||
"Close" = "Close";
|
||||
"Missing search parameter" = "Missing search parameter";
|
||||
"Missing type parameter" = "Missing type parameter";
|
||||
|
||||
/* Password Recovery */
|
||||
"Secondary e-mail" = "Secondary e-mail";
|
||||
"Answer" = "Answer";
|
||||
"Password lost" = "Password lost";
|
||||
"No password recovery method has been defined for this user" = "No password recovery method has been defined for this user";
|
||||
"Invalid secret question answer" = "Invalid secret question answer";
|
||||
"Next" = "Next";
|
||||
"Back" = "Back";
|
||||
"Please enter a new password below" = "Please enter a new password below";
|
||||
"A link will be sent to %{0}" = "A link will be sent to %{0}";
|
||||
"A password reset link has been sent, please check your recovery e-mail mailbox and click on the link" = "A password reset link has been sent, please check your recovery e-mail mailbox and click on the link";
|
||||
"Invalid configuration for email password recovery" = "Invalid configuration for email password recovery";
|
||||
"Password recovery email in error" = "Password recovery email in error";
|
||||
"Password reset" = "Password reset";
|
||||
"Hi %{0},\nThere was a request to change your password!\n\nIf you did not make this request then please ignore this email.\n\nOtherwise, please click this link to change your password: %{1}" = "Hi %{0},\nThere was a request to change your password!\n\nIf you did not make this request then please ignore this email.\n\nOtherwise, please click this link to change your password: %{1}";
|
||||
@@ -110,3 +110,19 @@
|
||||
"Close" = "Fermer";
|
||||
"Missing search parameter" = "Paramètre de recherche manquant";
|
||||
"Missing type parameter" = "Paramètre de type manquant";
|
||||
|
||||
/* Password Recovery */
|
||||
"Answer" = "Réponse";
|
||||
"Secondary e-mail" = "E-mail secondaire";
|
||||
"Password lost" = "Mot de passe perdu";
|
||||
"No password recovery method has been defined for this user" = "Aucune méthode de récupération de mot de passe n'a été définie pour cet utilisateur";
|
||||
"Invalid secret question answer" = "La réponse à la question secrète n'est pas valide";
|
||||
"Next" = "Suivant";
|
||||
"Back" = "Retour";
|
||||
"Please enter a new password below" = "Veuillez saisir un nouveau mot de passe ci-dessous";
|
||||
"A link will be sent to %{0}" = "Un lien va être envoyé à %{0}";
|
||||
"A password reset link has been sent, please check your recovery e-mail mailbox and click on the link" = "Un lien vous permettant de redéfinir votre mot de passe vous a été envoyé, veuillez vérifier votre boite e-mail de récupération";
|
||||
"Invalid configuration for email password recovery" = "Configuration invvalide pour la récupération de mot de passe par e-mail";
|
||||
"Password recovery email in error" = "Erreur lors de l'envoi de l'email de récupération";
|
||||
"Password reset" = "Réinitialisation de mot de passe";
|
||||
"Hi %{0},\nThere was a request to change your password!\n\nIf you did not make this request then please ignore this email.\n\nOtherwise, please click this link to change your password: %{1}" = "Bonjour %{0},\nUne demande de changement de mot de passe a été initiée.\n\nSi vous n'êtes pas à l'origine de cet e-mail, n'en tenez pas compte.\n\nSi vous en êtes bien à l'origine, veuillez cliquer sur le lien ci-dessous pour modifier votre mot de passe: %{1}";
|
||||
+246
-13
@@ -49,6 +49,8 @@
|
||||
#import <SOGo/SOGoUserManager.h>
|
||||
#import <SOGo/SOGoUserSettings.h>
|
||||
#import <SOGo/SOGoWebAuthenticator.h>
|
||||
#import <SOGo/SOGoEmptyAuthenticator.h>
|
||||
#import <SOGo/SOGoMailer.h>
|
||||
|
||||
#if defined(MFA_CONFIG)
|
||||
#include <liboath/oath.h>
|
||||
@@ -56,6 +58,8 @@
|
||||
|
||||
#import "SOGoRootPage.h"
|
||||
|
||||
static const NSString *kJwtKey = @"jwt";
|
||||
|
||||
@implementation SOGoRootPage
|
||||
|
||||
- (id) init
|
||||
@@ -202,7 +206,6 @@
|
||||
WOCookie *authCookie, *xsrfCookie;
|
||||
SOGoWebAuthenticator *auth;
|
||||
SOGoUserDefaults *ud;
|
||||
SOGoUserSettings *us;
|
||||
SOGoUser *loggedInUser;
|
||||
NSDictionary *params;
|
||||
NSString *username, *password, *language, *domain, *remoteHost;
|
||||
@@ -626,6 +629,11 @@
|
||||
return ([[self loginDomains] count] > 0);
|
||||
}
|
||||
|
||||
- (BOOL) hasPasswordRecovery
|
||||
{
|
||||
return [[SOGoSystemDefaults sharedSystemDefaults] isPasswordRecoveryEnabled];
|
||||
}
|
||||
|
||||
- (void) setItem: (id) _item
|
||||
{
|
||||
ASSIGN (item, _item);
|
||||
@@ -671,7 +679,7 @@
|
||||
|
||||
- (WOResponse *) changePasswordAction
|
||||
{
|
||||
NSString *username, *domain, *password, *newPassword, *value;
|
||||
NSString *username, *domain, *password, *newPassword, *value, *token;
|
||||
WOCookie *authCookie, *xsrfCookie;
|
||||
NSDictionary *message;
|
||||
NSArray *creds;
|
||||
@@ -681,6 +689,7 @@
|
||||
SOGoWebAuthenticator *auth;
|
||||
WOResponse *response;
|
||||
WORequest *request;
|
||||
BOOL passwordRecovery;
|
||||
|
||||
request = [context request];
|
||||
message = [[request contentAsString] objectFromJSONString];
|
||||
@@ -689,6 +698,7 @@
|
||||
value = [[context request] cookieValueForKey: [auth cookieNameInContext: context]];
|
||||
creds = nil;
|
||||
username = nil;
|
||||
passwordRecovery = NO;
|
||||
|
||||
if (value)
|
||||
{
|
||||
@@ -711,6 +721,12 @@
|
||||
newPassword = [message objectForKey: @"newPassword"];
|
||||
// overwrite the value from the session to compare the actual input
|
||||
password = [message objectForKey: @"oldPassword"];
|
||||
// get session id for password recovery
|
||||
token = [message objectForKey: @"token"];
|
||||
// If no old password but sessionid set, this is password recovery mode
|
||||
if ((!password || password == [NSNull null]) && token) {
|
||||
passwordRecovery = YES;
|
||||
}
|
||||
|
||||
// Validate required parameters
|
||||
if (!username)
|
||||
@@ -718,7 +734,7 @@
|
||||
response = [self responseWithStatus: 403
|
||||
andString: @"Missing 'username' parameter"];
|
||||
}
|
||||
else if (!password)
|
||||
else if (!password && !passwordRecovery)
|
||||
{
|
||||
response = [self responseWithStatus: 403
|
||||
andString: @"Missing 'oldPassword' parameter"];
|
||||
@@ -737,6 +753,8 @@
|
||||
inDomain: domain
|
||||
oldPassword: password
|
||||
newPassword: newPassword
|
||||
passwordRecovery: passwordRecovery
|
||||
token: token
|
||||
perr: &error])
|
||||
{
|
||||
if (creds)
|
||||
@@ -754,17 +772,19 @@
|
||||
}
|
||||
|
||||
response = [self responseWith204];
|
||||
authCookie = [auth cookieWithUsername: username
|
||||
andPassword: newPassword
|
||||
inContext: context];
|
||||
[response addCookie: authCookie];
|
||||
if (!passwordRecovery) {
|
||||
authCookie = [auth cookieWithUsername: username
|
||||
andPassword: newPassword
|
||||
inContext: context];
|
||||
[response addCookie: authCookie];
|
||||
|
||||
// We update the XSRF protection cookie
|
||||
creds = [auth parseCredentials: [authCookie value]];
|
||||
xsrfCookie = [WOCookie cookieWithName: @"XSRF-TOKEN"
|
||||
value: [[SOGoSession valueForSessionKey: [creds lastObject]] asSHA1String]];
|
||||
[xsrfCookie setPath: [NSString stringWithFormat: @"/%@/", [request applicationName]]];
|
||||
[response addCookie: xsrfCookie];
|
||||
// We update the XSRF protection cookie
|
||||
creds = [auth parseCredentials: [authCookie value]];
|
||||
xsrfCookie = [WOCookie cookieWithName: @"XSRF-TOKEN"
|
||||
value: [[SOGoSession valueForSessionKey: [creds lastObject]] asSHA1String]];
|
||||
[xsrfCookie setPath: [NSString stringWithFormat: @"/%@/", [request applicationName]]];
|
||||
[response addCookie: xsrfCookie];
|
||||
}
|
||||
}
|
||||
else
|
||||
response = [self _responseWithLDAPPolicyError: error];
|
||||
@@ -773,6 +793,219 @@
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* This action get password recovery data for specified user (mode, secret question, recovery email)
|
||||
* @return Response
|
||||
*/
|
||||
- (WOResponse *) passwordRecoveryAction
|
||||
{
|
||||
NSString *username, *domain;
|
||||
NSDictionary *jsonData, *message;
|
||||
WORequest *request;
|
||||
SOGoUserManager *um;
|
||||
|
||||
username = nil;
|
||||
domain = nil;
|
||||
request = [context request];
|
||||
|
||||
message = [[request contentAsString] objectFromJSONString];
|
||||
username = [message objectForKey: @"userName"];
|
||||
domain = [message objectForKey: @"domain"];
|
||||
um = [SOGoUserManager sharedUserManager];
|
||||
|
||||
|
||||
jsonData = [um getPasswordRecoveryInfosForUsername: username domain: domain];
|
||||
return [self responseWithStatus: 200
|
||||
andJSONRepresentation: jsonData];
|
||||
}
|
||||
|
||||
/**
|
||||
* This action generates JWT token and send password recovery mail
|
||||
* Then the JWT token is passed and controlled to changePassword (when clicking on link)
|
||||
* @return Response
|
||||
*/
|
||||
- (WOResponse *) passwordRecoveryEmailAction
|
||||
{
|
||||
NSString *username, *domain, *mode, *uid, *mailDomain, *fromEmail, *toEmail, *jwtToken, *url, *mailContent;
|
||||
NSDictionary *message, *info;
|
||||
WORequest *request;
|
||||
SOGoUserManager *um;
|
||||
SOGoUserDefaults *userDefaults;
|
||||
WOResponse *response;
|
||||
SOGoDomainDefaults *dd;
|
||||
SOGoUser *ownerUser;
|
||||
SOGoMailer *mailer;
|
||||
NSException *e;
|
||||
|
||||
username = nil;
|
||||
domain = nil;
|
||||
request = [context request];
|
||||
message = [[request contentAsString] objectFromJSONString];
|
||||
username = [message objectForKey: @"userName"];
|
||||
domain = [message objectForKey: @"domain"];
|
||||
mailDomain = [message objectForKey: @"mailDomain"];
|
||||
mode = [message objectForKey: @"mode"];
|
||||
um = [SOGoUserManager sharedUserManager];
|
||||
jwtToken = [request formValueForKey:@"token"];
|
||||
|
||||
if (!mode && jwtToken) {
|
||||
response = [self _standardDefaultAction];
|
||||
} else if ([mode isEqualToString: SOGoPasswordRecoverySecondaryEmail]) {
|
||||
if (mailDomain && username) {
|
||||
// Email recovery
|
||||
|
||||
// Create email from
|
||||
fromEmail = [NSString stringWithFormat:@"noreply@%@", mailDomain];
|
||||
|
||||
// Get password recovery email
|
||||
info = [um contactInfosForUserWithUIDorEmail: username];
|
||||
uid = [info objectForKey: @"c_uid"];
|
||||
userDefaults = [SOGoUserDefaults defaultsForUser: uid
|
||||
inDomain: domain];
|
||||
toEmail = [userDefaults passwordRecoverySecondaryEmail];
|
||||
|
||||
if (toEmail) {
|
||||
// Generate token
|
||||
jwtToken = [um generateAndSavePasswordRecoveryTokenWithUid: uid username: username domain: domain];
|
||||
|
||||
// Send mail
|
||||
ownerUser = [SOGoUser userWithLogin: username];
|
||||
dd = [ownerUser domainDefaults];
|
||||
mailer = [SOGoMailer mailerWithDomainDefaults: dd];
|
||||
url = [NSString stringWithFormat:@"%@%@?token=%@"
|
||||
, [[request headers] objectForKey:@"origin"]
|
||||
, [request uri]
|
||||
, jwtToken];
|
||||
|
||||
mailContent = [NSString stringWithFormat: @"Subject: %@\n\n%@"
|
||||
, [self labelForKey: @"Password reset"]
|
||||
, [self labelForKey: @"Hi %{0},\nThere was a request to change your password!\n\nIf you did not make this request then please ignore this email.\n\nOtherwise, please click this link to change your password: %{1}"]];
|
||||
// Replace message placeholders
|
||||
mailContent = [mailContent stringByReplacingOccurrencesOfString: @"%{0}" withString: username];
|
||||
mailContent = [mailContent stringByReplacingOccurrencesOfString: @"%{1}" withString: url];
|
||||
e = [mailer sendMailData: [mailContent dataUsingEncoding: NSUTF8StringEncoding]
|
||||
toRecipients: [NSArray arrayWithObjects: toEmail, nil]
|
||||
sender: fromEmail
|
||||
withAuthenticator: [SOGoEmptyAuthenticator sharedSOGoEmptyAuthenticator]
|
||||
inContext: [self context]];
|
||||
|
||||
if (!e) {
|
||||
response = [self responseWithStatus: 200
|
||||
andJSONRepresentation: nil];
|
||||
} else {
|
||||
[self logWithFormat: @"Password recovery exception for user %@ : %@", uid, [e reason]];
|
||||
response = [self responseWithStatus: 403
|
||||
andString: @"Password recovery email in error"];
|
||||
}
|
||||
} else {
|
||||
[self logWithFormat: @"No recovery email found for password recovery for user %@", uid];
|
||||
response = [self responseWithStatus: 403
|
||||
andString: @"Invalid configuration for email password recovery"];
|
||||
}
|
||||
} else {
|
||||
[self logWithFormat: @"No user domain found for password recovery"];
|
||||
response = [self responseWithStatus: 403
|
||||
andString: @"Invalid configuration for email password recovery"];
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* This action check secret question answer and generates JWT token.
|
||||
* Then the JWT token is passed and controlled to changePassword
|
||||
* @return Response
|
||||
*/
|
||||
- (WOResponse *) passwordRecoveryCheckAction
|
||||
{
|
||||
NSString *username, *domain, *token, *mode;
|
||||
NSDictionary *message;
|
||||
WORequest *request;
|
||||
SOGoUserManager *um;
|
||||
WOResponse *response;
|
||||
|
||||
username = nil;
|
||||
domain = nil;
|
||||
request = [context request];
|
||||
message = [[request contentAsString] objectFromJSONString];
|
||||
username = [message objectForKey: @"userName"];
|
||||
domain = [message objectForKey: @"domain"];
|
||||
mode = [message objectForKey: @"mode"];
|
||||
um = [SOGoUserManager sharedUserManager];
|
||||
|
||||
if ([mode isEqualToString: SOGoPasswordRecoveryQuestion]) {
|
||||
// Secret question recovery
|
||||
token = [um getTokenAndCheckPasswordRecoveryDataForUsername: username domain: domain withData: message];
|
||||
if (!token) {
|
||||
response = [self responseWithStatus: 403
|
||||
andString: @"Invalid secret question answer"];
|
||||
} else {
|
||||
response = [self responseWithStatus: 200
|
||||
andJSONRepresentation: [NSDictionary dictionaryWithObject: token forKey: kJwtKey]];
|
||||
}
|
||||
} else {
|
||||
response = [self responseWithStatus: 403
|
||||
andJSONRepresentation: nil];
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if password recovery is enabled for user domain
|
||||
* @return Response
|
||||
*/
|
||||
- (WOResponse *) passwordRecoveryEnabledAction
|
||||
{
|
||||
NSString *username, *domain, *domainName;
|
||||
NSArray *usernameComponents, *passwordRecoveryDomains;
|
||||
NSDictionary *message, *result;
|
||||
WORequest *request;
|
||||
|
||||
username = nil;
|
||||
domain = nil;
|
||||
domainName = nil;
|
||||
result = nil;
|
||||
request = [context request];
|
||||
|
||||
message = [[request contentAsString] objectFromJSONString];
|
||||
username = [message objectForKey: @"userName"];
|
||||
domain = [message objectForKey: @"domain"];
|
||||
if ([[SOGoSystemDefaults sharedSystemDefaults]
|
||||
isPasswordRecoveryEnabled]) {
|
||||
// If no domain, try to retrieve domain from username
|
||||
if (nil != domain && domain != [NSNull null]) {
|
||||
domainName = domain;
|
||||
} else if (nil != username) {
|
||||
usernameComponents = [username componentsSeparatedByString:@"@"]; // Split on @ and take right part
|
||||
if (2 == [usernameComponents count]) {
|
||||
domainName = [[usernameComponents objectAtIndex: 1] lowercaseString];
|
||||
}
|
||||
}
|
||||
|
||||
result = [NSDictionary dictionaryWithObject: domainName forKey: @"domain"];
|
||||
}
|
||||
|
||||
passwordRecoveryDomains = [[SOGoSystemDefaults sharedSystemDefaults]
|
||||
passwordRecoveryDomains];
|
||||
if (![[SOGoSystemDefaults sharedSystemDefaults] isPasswordRecoveryEnabled]) {
|
||||
return [self responseWithStatus: 403
|
||||
andJSONRepresentation: nil];
|
||||
} else if (username && [NSNull null] != username &&
|
||||
domainName && passwordRecoveryDomains &&
|
||||
[passwordRecoveryDomains containsObject: domainName]) {
|
||||
return [self responseWithStatus: 200
|
||||
andJSONRepresentation: result];
|
||||
} else if (username && [NSNull null] != username && !passwordRecoveryDomains) {
|
||||
return [self responseWithStatus: 200
|
||||
andJSONRepresentation: result];
|
||||
} else {
|
||||
return [self responseWithStatus: 403
|
||||
andJSONRepresentation: nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL) isTotpEnabled
|
||||
{
|
||||
#if defined(MFA_CONFIG)
|
||||
|
||||
@@ -173,6 +173,26 @@
|
||||
pageName = "SOGoRootPage";
|
||||
actionName = "changePassword";
|
||||
};
|
||||
passwordRecovery = {
|
||||
protectedBy = "<public>";
|
||||
pageName = "SOGoRootPage";
|
||||
actionName = "passwordRecovery";
|
||||
};
|
||||
passwordRecoveryEmail = {
|
||||
protectedBy = "<public>";
|
||||
pageName = "SOGoRootPage";
|
||||
actionName = "passwordRecoveryEmail";
|
||||
};
|
||||
passwordRecoveryCheck = {
|
||||
protectedBy = "<public>";
|
||||
pageName = "SOGoRootPage";
|
||||
actionName = "passwordRecoveryCheck";
|
||||
};
|
||||
passwordRecoveryEnabled = {
|
||||
protectedBy = "<public>";
|
||||
pageName = "SOGoRootPage";
|
||||
actionName = "passwordRecoveryEnabled";
|
||||
};
|
||||
loading = {
|
||||
protectedBy = "<public>";
|
||||
pageName = "UIxLoading";
|
||||
|
||||
@@ -489,4 +489,12 @@
|
||||
/* 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.";
|
||||
"Let SOGo handle everything" = "Let SOGo handle everything";
|
||||
"Let SOGo handle everything" = "Let SOGo handle everything";
|
||||
|
||||
/* Password Recovery */
|
||||
"Password recovery mode" = "Password recovery";
|
||||
"Question" = "Question";
|
||||
"Secondary e-mail" = "Secondary e-mail";
|
||||
"Answer" = "Answer";
|
||||
"Password recovery" = "Password recovery";
|
||||
"Password change" = "Password change";
|
||||
|
||||
@@ -489,4 +489,12 @@
|
||||
/* External Sieve scripts */
|
||||
"An external Sieve script is active" = "Un script Sieve externe est actif";
|
||||
"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 est un langage de programmation qui peut être utilisé pour le filtrage des messages. Si vous laissez SOGo gérer vos filtres, votre message d'absence et vos paramètres de transfert, votre script actuel sera désactivé.";
|
||||
"Let SOGo handle everything" = "Laissez SOGo s'occuper de tout";
|
||||
"Let SOGo handle everything" = "Laissez SOGo s'occuper de tout";
|
||||
|
||||
/* Password Recovery */
|
||||
"Password recovery mode" = "Récupération de mot de passe";
|
||||
"Question" = "Question";
|
||||
"Answer" = "Réponse";
|
||||
"Secondary e-mail" = "E-mail secondaire";
|
||||
"Password recovery" = "Récupération de mot de passe";
|
||||
"Password change" = "Changement de mot de passe";
|
||||
@@ -206,6 +206,18 @@ static SoProduct *preferencesProduct = nil;
|
||||
if (![[defaults source] objectForKey: @"SOGoCalendarDefaultReminder"])
|
||||
[[defaults source] setObject: [defaults calendarDefaultReminder] forKey: @"SOGoCalendarDefaultReminder"];
|
||||
|
||||
if (![[defaults source] objectForKey: @"SOGoPasswordRecoveryMode"])
|
||||
[[defaults source] setObject: [defaults passwordRecoveryMode] forKey: @"SOGoPasswordRecoveryMode"];
|
||||
|
||||
if (![[defaults source] objectForKey: @"SOGoPasswordRecoveryQuestion"])
|
||||
[[defaults source] setObject: [defaults passwordRecoveryQuestion] forKey: @"SOGoPasswordRecoveryQuestion"];
|
||||
|
||||
if (![[defaults source] objectForKey: @"SOGoPasswordRecoveryQuestionAnswer"])
|
||||
[[defaults source] setObject: [defaults passwordRecoveryQuestionAnswer] forKey: @"SOGoPasswordRecoveryQuestionAnswer"];
|
||||
|
||||
if (![[defaults source] objectForKey: @"SOGoPasswordRecoverySecondaryEmail"])
|
||||
[[defaults source] setObject: [defaults passwordRecoverySecondaryEmail] forKey: @"SOGoPasswordRecoverySecondaryEmail"];
|
||||
|
||||
// Populate default calendar categories, based on the user's preferred language
|
||||
if (![[defaults source] objectForKey: @"SOGoCalendarCategories"])
|
||||
{
|
||||
|
||||
@@ -1638,4 +1638,40 @@ static NSArray *reminderValues = nil;
|
||||
return results;
|
||||
}
|
||||
|
||||
// Password recovery
|
||||
|
||||
- (NSArray *) passwordRecoveryList
|
||||
{
|
||||
return [NSArray arrayWithObjects:
|
||||
SOGoPasswordRecoveryDisabled,
|
||||
SOGoPasswordRecoveryQuestion,
|
||||
SOGoPasswordRecoverySecondaryEmail,
|
||||
nil];
|
||||
}
|
||||
|
||||
- (NSArray *) passwordRecoveryQuestionList
|
||||
{
|
||||
return [NSArray arrayWithObjects:
|
||||
SOGoPasswordRecoveryQuestion1,
|
||||
SOGoPasswordRecoveryQuestion2,
|
||||
SOGoPasswordRecoveryQuestion3,
|
||||
nil];
|
||||
}
|
||||
|
||||
- (NSString *) itemPasswordRecoveryText
|
||||
{
|
||||
return [self commonLabelForKey: [NSString stringWithFormat: @"passwordRecovery_%@",
|
||||
item]];
|
||||
}
|
||||
|
||||
- (BOOL) shouldDisplayPasswordRecovery
|
||||
{
|
||||
return [[SOGoSystemDefaults sharedSystemDefaults] isPasswordRecoveryEnabled];
|
||||
}
|
||||
|
||||
- (BOOL) shouldDisplayPasswordSection
|
||||
{
|
||||
return [self shouldDisplayPasswordRecovery] || [self shouldDisplayPasswordChange];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -792,7 +792,12 @@ static SoProduct *commonProduct = nil;
|
||||
// needs to be created (or destroyed) during the session initialization
|
||||
if ([_actionName isEqualToString: @"connect"] ||
|
||||
[_actionName isEqualToString: @"changePassword"] ||
|
||||
[_actionName isEqualToString: @"logoff"])
|
||||
[_actionName isEqualToString: @"logoff"] ||
|
||||
[_actionName isEqualToString: @"passwordRecovery"] ||
|
||||
[_actionName isEqualToString: @"passwordRecoveryEmail"] ||
|
||||
[_actionName isEqualToString: @"passwordRecoveryCheck"] ||
|
||||
[_actionName isEqualToString: @"passwordRecoveryEnabled"]
|
||||
)
|
||||
{
|
||||
return [super performActionNamed: _actionName];
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
xmlns:label="OGo:label"
|
||||
xmlns:rsrc="OGo:url">
|
||||
|
||||
<div layout="column" class="layout-fill sg-reversible">
|
||||
|
||||
<md-card style="overflow: hidden">
|
||||
<md-toolbar>
|
||||
<div class="md-toolbar-tools">
|
||||
|
||||
@@ -41,72 +41,84 @@
|
||||
<var:if condition="hasLoginSuffix">
|
||||
<input type="hidden" ng-model="app.creds.loginSuffix" var:value="loginSuffix"/>
|
||||
</var:if>
|
||||
<md-input-container class="md-block">
|
||||
<label><var:string label:value="Username"/></label>
|
||||
<md-icon>person</md-icon>
|
||||
<input autocorrect="off" autocapitalize="off" type="text" ng-model="app.creds.username" ng-required="true"/>
|
||||
</md-input-container>
|
||||
<md-input-container class="md-block">
|
||||
<label><var:string label:value="Password"/></label>
|
||||
<md-icon>email</md-icon>
|
||||
<input type="password" ng-model="app.creds.password" ng-required="true"/>
|
||||
</md-input-container>
|
||||
|
||||
<!-- LANGUAGES SELECT -->
|
||||
<div layout="row" layout-align="start end">
|
||||
<md-icon>language</md-icon>
|
||||
<md-input-container class="md-flex">
|
||||
<label><var:string label:value="choose"/></label>
|
||||
<md-select ng-model="app.creds.language"
|
||||
var:placeholder="localizedLanguage"
|
||||
ng-change="app.changeLanguage($event)">
|
||||
<var:foreach list="languages" item="item">
|
||||
<md-option var:value="item">
|
||||
<var:string value="languageText"/>
|
||||
</md-option>
|
||||
</var:foreach>
|
||||
</md-select>
|
||||
<div ng-if="!app.loginState">
|
||||
<md-input-container class="md-block">
|
||||
<label><var:string label:value="Username"/></label>
|
||||
<md-icon>person</md-icon>
|
||||
<input autocorrect="off" autocapitalize="off" type="text" ng-model="app.creds.username" ng-required="true" ng-change="app.usernameChanged()" ng-blur="app.retrievePasswordRecoveryEnabled()" />
|
||||
</md-input-container>
|
||||
</div>
|
||||
<md-input-container class="md-block">
|
||||
<label><var:string label:value="Password"/></label>
|
||||
<md-icon>email</md-icon>
|
||||
<input type="password" ng-model="app.creds.password" ng-required="true"/>
|
||||
</md-input-container>
|
||||
|
||||
|
||||
<!-- DOMAINS SELECT -->
|
||||
<var:if condition="hasLoginDomains">
|
||||
<!-- LANGUAGES SELECT -->
|
||||
<div layout="row" layout-align="start end">
|
||||
<md-icon>domain</md-icon>
|
||||
<md-icon>language</md-icon>
|
||||
<md-input-container class="md-flex">
|
||||
<md-select class="md-flex" ng-model="app.creds.domain" label:placeholder="choose">
|
||||
<var:foreach list="loginDomains" item="item">
|
||||
<label><var:string label:value="choose"/></label>
|
||||
<md-select ng-model="app.creds.language"
|
||||
var:placeholder="localizedLanguage"
|
||||
ng-change="app.changeLanguage($event)">
|
||||
<var:foreach list="languages" item="item">
|
||||
<md-option var:value="item">
|
||||
<var:string value="item"/>
|
||||
<var:string value="languageText"/>
|
||||
</md-option>
|
||||
</var:foreach>
|
||||
</md-select>
|
||||
</md-input-container>
|
||||
</div>
|
||||
</var:if>
|
||||
|
||||
<div layout="row" layout-align="center center">
|
||||
<md-switch class="md-accent md-hue-2"
|
||||
ng-model="app.creds.rememberLogin"
|
||||
label:arial-label="Remember username">
|
||||
<var:string label:value="Remember username"/>
|
||||
</md-switch>
|
||||
|
||||
<!-- DOMAINS SELECT -->
|
||||
<var:if condition="hasLoginDomains">
|
||||
<div layout="row" layout-align="start end">
|
||||
<md-icon>domain</md-icon>
|
||||
<md-input-container class="md-flex">
|
||||
<md-select class="md-flex" ng-model="app.creds.domain" label:placeholder="choose" ng-change="app.retrievePasswordRecoveryEnabled()">
|
||||
<var:foreach list="loginDomains" item="item">
|
||||
<md-option var:value="item">
|
||||
<var:string value="item"/>
|
||||
</md-option>
|
||||
</var:foreach>
|
||||
</md-select>
|
||||
</md-input-container>
|
||||
</div>
|
||||
</var:if>
|
||||
|
||||
<div layout="row" layout-align="center center">
|
||||
<md-switch class="md-accent md-hue-2"
|
||||
ng-model="app.creds.rememberLogin"
|
||||
label:arial-label="Remember username">
|
||||
<var:string label:value="Remember username"/>
|
||||
</md-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password recovery -->
|
||||
<div layout="row" layout-align="center center" ng-if="app.passwordRecovery.passwordRecoveryEnabled">
|
||||
<div ng-if="app.showLogin">
|
||||
<a href="#" ng-click="app.passwordRecoveryInfo()" sg-ripple-click="loginContent"><var:string label:value="Password lost"/></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CONNECT BUTTON -->
|
||||
<div layout="row" layout-align="space-between center">
|
||||
<div layout="row" layout-align="space-between center" ng-if="!app.loginState">
|
||||
<md-button class="md-icon-button"
|
||||
label:aria-label="About"
|
||||
ng-click="app.showAbout()">
|
||||
<md-icon>info</md-icon>
|
||||
</md-button>
|
||||
<md-button class="md-fab md-accent md-hue-2" type="submit"
|
||||
label:aria-label="Connect"
|
||||
ng-if="!app.loginState"
|
||||
ng-disabled="loginForm.$invalid"
|
||||
sg-ripple-click="loginContent">
|
||||
<md-icon>arrow_forward</md-icon>
|
||||
</md-button>
|
||||
<div>
|
||||
<md-button class="md-fab md-accent md-hue-2" type="submit"
|
||||
label:aria-label="Connect"
|
||||
ng-if="!app.loginState"
|
||||
ng-disabled="loginForm.$invalid"
|
||||
sg-ripple-click="loginContent">
|
||||
<md-icon>arrow_forward</md-icon>
|
||||
</md-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<sg-ripple class="md-default-theme md-accent md-bg"
|
||||
@@ -181,16 +193,20 @@
|
||||
</div>
|
||||
</var:if>
|
||||
|
||||
<!-- Password policy: Password is expired -->
|
||||
<!-- Password policy: Password is expired / password recovery-->
|
||||
<div layout="column" layout-align="center center"
|
||||
ng-switch-when="passwordexpired">
|
||||
<md-icon class="md-accent md-hue-1 sg-icon--large">watch_later</md-icon>
|
||||
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
|
||||
ng-switch-when="passwordchange">
|
||||
<md-icon class="md-accent md-hue-1 sg-icon--large" ng-if="!app.isInPasswordRecoveryMode()">watch_later</md-icon>
|
||||
<md-icon class="md-accent md-hue-1 sg-icon--large" ng-if="app.isInPasswordRecoveryMode()">vpn_key</md-icon>
|
||||
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding" ng-if="!app.isInPasswordRecoveryMode()">
|
||||
<var:string label:value="Your password has expired, please enter a new one below"/>
|
||||
</div>
|
||||
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding" ng-if="app.isInPasswordRecoveryMode()">
|
||||
<var:string label:value="Please enter a new password below"/>
|
||||
</div>
|
||||
<div flex="100">
|
||||
<div layout="row" layout-xs="column">
|
||||
<md-input-container class="md-block" flex="flex">
|
||||
<md-input-container class="md-block" flex="flex" ng-if="!app.isInPasswordRecoveryMode()">
|
||||
<label><var:string label:value="Current password"/>
|
||||
</label>
|
||||
<input type="password" sg-no-dirty-check="true" ng-model="app.passwords.oldPassword"/>
|
||||
@@ -239,6 +255,59 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password recovery -->
|
||||
<var:if condition="hasPasswordRecovery">
|
||||
<div layout="column" layout-align="center center"
|
||||
ng-switch-when="passwordrecovery">
|
||||
<md-icon class="md-accent md-hue-1 sg-icon--large">vpn_key</md-icon>
|
||||
<div flex="100">
|
||||
<div layout="row" layout-xs="column" class="md-padding">
|
||||
<div ng-if="'SecretQuestion' === app.passwordRecovery.passwordRecoveryMode">
|
||||
{{ app.passwordRecovery.passwordRecoveryQuestion }}
|
||||
<md-input-container class="md-block">
|
||||
<label><var:string label:value="Answer"/></label>
|
||||
<input autocorrect="off" autocapitalize="off" type="text" ng-model="app.passwordRecovery.passwordRecoveryQuestionAnswer" />
|
||||
</md-input-container>
|
||||
</div>
|
||||
<div ng-if="'SecondaryEmail' === app.passwordRecovery.passwordRecoveryMode">
|
||||
{{ app.passwordRecovery.passwordRecoverySecondaryEmailText }}
|
||||
</div>
|
||||
</div>
|
||||
<div layout="row" layout-align="end center">
|
||||
<md-button ng-click="app.passwordRecoveryAbort()" type="button" >
|
||||
<var:string label:value="Back"/>
|
||||
</md-button>
|
||||
<div ng-if="'SecretQuestion' === app.passwordRecovery.passwordRecoveryMode">
|
||||
<md-button ng-click="app.passwordRecoveryCheck()" type="button" >
|
||||
<var:string label:value="Next"/>
|
||||
</md-button>
|
||||
</div>
|
||||
<div ng-if="'SecondaryEmail' === app.passwordRecovery.passwordRecoveryMode">
|
||||
<md-button ng-click="app.passwordRecoveryEmail()" type="button" >
|
||||
<var:string label:value="Next"/>
|
||||
</md-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div layout="column" layout-align="center center"
|
||||
ng-switch-when="sendrecoverymail">
|
||||
<md-icon class="md-accent md-hue-1 sg-icon--large">local_shipping</md-icon>
|
||||
<div flex="100">
|
||||
<div layout="row" layout-xs="column" class="md-padding">
|
||||
<div ng-if="'SecondaryEmail' === app.passwordRecovery.passwordRecoveryMode">
|
||||
<var:string label:value="A password reset link has been sent, please check your recovery e-mail mailbox and click on the link"/>
|
||||
</div>
|
||||
</div>
|
||||
<div layout="row" layout-align="end center">
|
||||
<md-button ng-click="app.passwordRecoveryAbort()" type="button" >
|
||||
<var:string label:value="Back"/>
|
||||
</md-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</var:if>
|
||||
|
||||
<!-- Logged in -->
|
||||
<div layout="column" layout-align="center center"
|
||||
ng-switch-when="logged">
|
||||
@@ -277,6 +346,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</md-content>
|
||||
|
||||
</main>
|
||||
|
||||
<script type="text/ng-template" id="aboutBox.html">
|
||||
|
||||
@@ -267,47 +267,88 @@
|
||||
</md-tab>
|
||||
|
||||
<!-- PASSWORD OPTIONS -->
|
||||
<var:if condition="shouldDisplayPasswordChange">
|
||||
<var:if condition="shouldDisplayPasswordSection">
|
||||
<md-tab id="generalPasswordView"
|
||||
aria-controls="generalPasswordView-content"
|
||||
label:label="Password">
|
||||
<md-content id="passwordView" class="md-padding">
|
||||
<div layout="row" layout-xs="column">
|
||||
<md-input-container class="md-block" flex="flex">
|
||||
<label><var:string label:value="Current password"/>
|
||||
</label>
|
||||
<input type="password" sg-no-dirty-check="true" ng-model="app.passwords.oldPassword"/>
|
||||
</md-input-container>
|
||||
<md-input-container class="md-block" flex="flex">
|
||||
<label><var:string label:value="New password"/>
|
||||
</label>
|
||||
<input type="password" sg-no-dirty-check="true" ng-model="app.passwords.newPassword"/>
|
||||
<div class="sg-hint">
|
||||
<ul class="sg-padded">
|
||||
<var:foreach list="passwordPolicy" item="item">
|
||||
<li><var:string value="item.label"/></li>
|
||||
</var:foreach>
|
||||
</ul>
|
||||
aria-controls="generalPasswordView-content"
|
||||
label:label="Password">
|
||||
<md-content id="passwordView" class="md-padding">
|
||||
<!-- Password recovery -->
|
||||
<var:if condition="shouldDisplayPasswordRecovery">
|
||||
<h5><var:string label:value="Password recovery"/></h5>
|
||||
<div layout="row" layout-align="start start">
|
||||
<md-input-container class="md-block md-input-has-value">
|
||||
<label><var:string label:value="Password recovery mode"/></label>
|
||||
<md-select ng-model="app.preferences.defaults.SOGoPasswordRecoveryMode">
|
||||
<var:foreach list="passwordRecoveryList" item="item">
|
||||
<md-option var:value="item">
|
||||
<var:string value="itemPasswordRecoveryText"/>
|
||||
</md-option>
|
||||
</var:foreach>
|
||||
</md-select>
|
||||
</md-input-container>
|
||||
</div>
|
||||
</md-input-container>
|
||||
<md-input-container class="md-block" flex="flex">
|
||||
<label><var:string label:value="Confirmation"/>
|
||||
</label>
|
||||
<input type="password" name="newPasswordConfirmation" sg-no-dirty-check="true" ng-model="app.passwords.newPasswordConfirmation"/>
|
||||
<div ng-messages="preferencesForm.newPasswordConfirmation.$error">
|
||||
<div ng-message="newPasswordMismatch"><var:string label:value="Passwords don't match"/></div>
|
||||
<div layout="row" layout-align="start start" ng-if="app.preferences.isItemSecretQuestion()">
|
||||
<md-input-container class="md-block md-input-has-value">
|
||||
<label><var:string label:value="Question"/></label>
|
||||
<md-select ng-model="app.preferences.defaults.SOGoPasswordRecoveryQuestion">
|
||||
<var:foreach list="passwordRecoveryQuestionList" item="item">
|
||||
<md-option var:value="item">
|
||||
<var:string value="itemPasswordRecoveryText"/>
|
||||
</md-option>
|
||||
</var:foreach>
|
||||
</md-select>
|
||||
</md-input-container>
|
||||
<md-input-container class="md-block">
|
||||
<label><var:string label:value="Answer"/></label>
|
||||
<input autocorrect="off" autocapitalize="off" type="text" ng-model="app.preferences.defaults.SOGoPasswordRecoveryQuestionAnswer" ng-required="app.preferences.isItemSecretQuestion()"/>
|
||||
</md-input-container>
|
||||
</div>
|
||||
</md-input-container>
|
||||
</div>
|
||||
<div layout="row" layout-align="end center">
|
||||
<md-button ng-click="app.changePassword()" type="button" ng-disabled="!app.canChangePassword(preferencesForm)">
|
||||
<var:string label:value="Change"/>
|
||||
</md-button>
|
||||
</div>
|
||||
<div layout="row" layout-align="start start" ng-if="app.preferences.isItemSecondaryEmail()">
|
||||
<md-input-container class="md-block">
|
||||
<label><var:string label:value="Secondary e-mail"/></label>
|
||||
<input autocorrect="off" autocapitalize="off" type="text" ng-model="app.preferences.defaults.SOGoPasswordRecoverySecondaryEmail" ng-required="app.preferences.isItemSecondaryEmail()" pattern="[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,3}$"/>
|
||||
</md-input-container>
|
||||
</div>
|
||||
</var:if>
|
||||
<!-- Password change -->
|
||||
<var:if condition="shouldDisplayPasswordChange">
|
||||
<h5><var:string label:value="Password change"/></h5>
|
||||
<div layout="row" layout-xs="column">
|
||||
<md-input-container class="md-block" flex="flex">
|
||||
<label><var:string label:value="Current password"/>
|
||||
</label>
|
||||
<input type="password" sg-no-dirty-check="true" ng-model="app.passwords.oldPassword"/>
|
||||
</md-input-container>
|
||||
<md-input-container class="md-block" flex="flex">
|
||||
<label><var:string label:value="New password"/>
|
||||
</label>
|
||||
<input type="password" sg-no-dirty-check="true" ng-model="app.passwords.newPassword"/>
|
||||
<div class="sg-hint">
|
||||
<ul class="sg-padded">
|
||||
<var:foreach list="passwordPolicy" item="item">
|
||||
<li><var:string value="item.label"/></li>
|
||||
</var:foreach>
|
||||
</ul>
|
||||
</div>
|
||||
</md-input-container>
|
||||
<md-input-container class="md-block" flex="flex">
|
||||
<label><var:string label:value="Confirmation"/>
|
||||
</label>
|
||||
<input type="password" name="newPasswordConfirmation" sg-no-dirty-check="true" ng-model="app.passwords.newPasswordConfirmation"/>
|
||||
<div ng-messages="preferencesForm.newPasswordConfirmation.$error">
|
||||
<div ng-message="newPasswordMismatch"><var:string label:value="Passwords don't match"/></div>
|
||||
</div>
|
||||
</md-input-container>
|
||||
</div>
|
||||
<div layout="row" layout-align="end center">
|
||||
<md-button ng-click="app.changePassword()" type="button" ng-disabled="!app.canChangePassword(preferencesForm)">
|
||||
<var:string label:value="Change"/>
|
||||
</md-button>
|
||||
</div>
|
||||
</var:if>
|
||||
</md-content>
|
||||
</md-tab>
|
||||
</var:if>
|
||||
|
||||
</md-tabs>
|
||||
</script>
|
||||
|
||||
|
||||
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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user