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

This commit is contained in:
smizrahi
2022-10-03 08:56:41 +02:00
parent 6931186bba
commit 656807b77d
47 changed files with 1729 additions and 159 deletions
+216 -3
View File
@@ -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