Skip to content

Commit

Permalink
feat(password-recovery): Add password recovery with secret question o…
Browse files Browse the repository at this point in the history
…r secondary email
  • Loading branch information
WoodySlum committed Oct 3, 2022
1 parent 6931186 commit 656807b
Show file tree
Hide file tree
Showing 47 changed files with 1,730 additions and 160 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
23 changes: 23 additions & 0 deletions Documentation/SOGoInstallationGuide.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions SoObjects/SOGo/GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ SOGo_HEADER_FILES = \
SOGoDAVAuthenticator.h \
SOGoProxyAuthenticator.h \
SOGoStaticAuthenticator.h \
SOGoEmptyAuthenticator.h \
SOGoWebAuthenticator.h \
SOGoWebDAVAclManager.h \
SOGoWebDAVValue.h \
Expand All @@ -88,7 +89,9 @@ SOGo_HEADER_FILES = \
\
SOGoCredentialsFile.h \
SOGoTextTemplateFile.h \
SOGoZipArchiver.h
SOGoZipArchiver.h \
\
JWT.h

all::
@touch SOGoBuild.m
Expand Down Expand Up @@ -154,6 +157,7 @@ SOGo_OBJC_FILES = \
SOGoDAVAuthenticator.m \
SOGoProxyAuthenticator.m \
SOGoStaticAuthenticator.m \
SOGoEmptyAuthenticator.m \
SOGoWebAuthenticator.m \
SOGoWebDAVAclManager.m \
SOGoWebDAVValue.m \
Expand All @@ -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

Expand Down
39 changes: 39 additions & 0 deletions SoObjects/SOGo/JWT.h
Original file line number Diff line number Diff line change
@@ -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 */
268 changes: 268 additions & 0 deletions SoObjects/SOGo/JWT.m
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 656807b

Please sign in to comment.