diff --git a/SoObjects/SOGo/GNUmakefile.preamble b/SoObjects/SOGo/GNUmakefile.preamble index 24d3493e6..30bc91903 100644 --- a/SoObjects/SOGo/GNUmakefile.preamble +++ b/SoObjects/SOGo/GNUmakefile.preamble @@ -43,6 +43,10 @@ ADDITIONAL_CPPFLAGS += $(LASSO_CFLAGS) SOGo_LIBRARIES_DEPEND_UPON += $(LASSO_LIBS) endif +ifeq ($(HAS_LIBRARY_oath), yes) +SOGo_LIBRARIES_DEPEND_UPON += $(MFA_LIBS) +endif + ifeq ($(findstring openbsd, $(GNUSTEP_HOST_OS)), openbsd) SOGo_LIBRARIES_DEPEND_UPON += -lcrypto else diff --git a/SoObjects/SOGo/SOGoCache.m b/SoObjects/SOGo/SOGoCache.m index 065f1dfe5..9fab8be6a 100644 --- a/SoObjects/SOGo/SOGoCache.m +++ b/SoObjects/SOGo/SOGoCache.m @@ -1,6 +1,6 @@ /* SOGoCache.m - this file is part of SOGo * - * Copyright (C) 2008-2014 Inverse inc. + * Copyright (C) 2008-2020 Inverse inc. * * 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 diff --git a/SoObjects/SOGo/SOGoUser.h b/SoObjects/SOGo/SOGoUser.h index d72873bee..a75929ccf 100644 --- a/SoObjects/SOGo/SOGoUser.h +++ b/SoObjects/SOGo/SOGoUser.h @@ -1,5 +1,5 @@ /* - Copyright (C) 2006-2016 Inverse inc. + Copyright (C) 2006-2020 Inverse inc. This file is part of SOGo. @@ -120,6 +120,7 @@ - (BOOL) isSuperUser; - (BOOL) canAuthenticate; +- (NSString *) googleAuthenticatorKey; /* resource */ - (BOOL) isResource; diff --git a/SoObjects/SOGo/SOGoUser.m b/SoObjects/SOGo/SOGoUser.m index 3b4aa7593..0e9888fec 100644 --- a/SoObjects/SOGo/SOGoUser.m +++ b/SoObjects/SOGo/SOGoUser.m @@ -1,5 +1,5 @@ /* - Copyright (C) 2006-2016 Inverse inc. + Copyright (C) 2006-2020 Inverse inc. This file is part of SOGo. @@ -39,6 +39,10 @@ #import "SOGoUserSettings.h" #import "WOResourceManager+SOGo.h" +#if defined(MFA_CONFIG) +#include +#endif + #import "SOGoUser.h" @implementation SoUser (SOGoExtension) @@ -1079,6 +1083,34 @@ return [authValue boolValue]; } +- (NSString *) googleAuthenticatorKey +{ +#if defined(MFA_CONFIG) + NSString *key, *result; + const char *s; + char *secret; + + size_t s_len, secret_len; + + key = [[[self userSettings] userSalt] substringToIndex: 12]; + s = [key UTF8String]; + s_len = strlen(s); + + oath_init(); + oath_base32_encode(s,s_len, &secret, &secret_len); + oath_done(); + + result = [[NSString alloc] initWithBytesNoCopy: secret + length: secret_len + encoding: NSASCIIStringEncoding + freeWhenDone: YES]; + + return [result autorelease]; +#else + return nil; +#endif +} + /* resource */ - (BOOL) isResource { diff --git a/SoObjects/SOGo/SOGoUserDefaults.h b/SoObjects/SOGo/SOGoUserDefaults.h index 2970530ae..bbfd96376 100644 --- a/SoObjects/SOGo/SOGoUserDefaults.h +++ b/SoObjects/SOGo/SOGoUserDefaults.h @@ -133,6 +133,9 @@ extern NSString *SOGoWeekStartFirstFullWeek; - (void) setAnimationMode: (NSString *) newValue; - (NSString *) animationMode; +- (BOOL) googleAuthenticatorEnabled; +- (void) setGoogleAuthenticatorEnabled: (BOOL) newValue; + - (void) setMailComposeWindow: (NSString *) newValue; - (NSString *) mailComposeWindow; diff --git a/SoObjects/SOGo/SOGoUserDefaults.m b/SoObjects/SOGo/SOGoUserDefaults.m index 28de36cd0..35e0647c4 100644 --- a/SoObjects/SOGo/SOGoUserDefaults.m +++ b/SoObjects/SOGo/SOGoUserDefaults.m @@ -1,6 +1,6 @@ /* SOGoUserDefaults.m - this file is part of SOGo * - * Copyright (C) 2009-2017 Inverse inc. + * Copyright (C) 2009-2020 Inverse inc. * * 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 @@ -542,6 +542,16 @@ NSString *SOGoWeekStartFirstFullWeek = @"FirstFullWeek"; return [self stringForKey: @"SOGoAnimationMode"]; } +- (BOOL) googleAuthenticatorEnabled +{ + return [self boolForKey: @"SOGoGoogleAuthenticatorEnabled"]; +} + +- (void) setGoogleAuthenticatorEnabled: (BOOL) newValue +{ + [self setBool: newValue forKey: @"SOGoGoogleAuthenticatorEnabled"]; +} + - (void) setMailComposeWindow: (NSString *) newValue { [self setObject: newValue forKey: @"SOGoMailComposeWindow"]; diff --git a/UI/MainUI/GNUmakefile.preamble b/UI/MainUI/GNUmakefile.preamble index ec6ba0c4f..b2336c142 100644 --- a/UI/MainUI/GNUmakefile.preamble +++ b/UI/MainUI/GNUmakefile.preamble @@ -8,3 +8,7 @@ ADDITIONAL_CPPFLAGS += \ ifeq ($(HAS_LIBRARY_lasso), yes) ADDITIONAL_CPPFLAGS += $(LASSO_CFLAGS) endif + +ifeq ($(HAS_LIBRARY_oath), yes) +ADDITIONAL_LDFLAGS += $(MFA_LIBS) +endif diff --git a/UI/MainUI/SOGoRootPage.m b/UI/MainUI/SOGoRootPage.m index 95660794c..0349da6db 100644 --- a/UI/MainUI/SOGoRootPage.m +++ b/UI/MainUI/SOGoRootPage.m @@ -50,6 +50,10 @@ #import #import +#if defined(MFA_CONFIG) +#include +#endif + #import "SOGoRootPage.h" @implementation SOGoRootPage @@ -182,7 +186,7 @@ SOGoUserDefaults *ud; SOGoUser *loggedInUser; NSDictionary *params; - NSString *username, *password, *language, *domain, *remoteHost; + NSString *username, *password, *language, *domain, *remoteHost, *verificationCode; NSArray *supportedLanguages, *creds; SOGoPasswordPolicyError err; @@ -198,6 +202,7 @@ username = [params objectForKey: @"userName"]; password = [params objectForKey: @"password"]; + verificationCode = [params objectForKey: @"verificationCode"]; language = [params objectForKey: @"language"]; rememberLogin = [[params objectForKey: @"rememberLogin"] boolValue]; domain = [params objectForKey: @"domain"]; @@ -223,12 +228,70 @@ // the DomainLessLogin situation, so we would NOT add the domain. -getUIDForEmail // has all the logic for this, so lets use it. if ([domain isNotNull]) - { - username = [[SOGoUserManager sharedUserManager] getUIDForEmail: username]; - } + username = [[SOGoUserManager sharedUserManager] getUIDForEmail: username]; loggedInUser = [SOGoUser userWithLogin: username]; +#if defined(MFA_CONFIG) + if ([[loggedInUser userDefaults] googleAuthenticatorEnabled]) + { + if ([verificationCode length] == 6 && [verificationCode unsignedIntValue] > 0) + { + unsigned int code; + const char *real_secret; + char *secret; + + size_t secret_len; + + const auto time_step = OATH_TOTP_DEFAULT_TIME_STEP_SIZE; + const auto digits = 6; + + real_secret = [[loggedInUser googleAuthenticatorKey] UTF8String]; + + auto result = oath_init(); + auto t = time(NULL); + auto left = time_step - (t % time_step); + + char otp[digits + 1]; + + oath_base32_decode (real_secret, + strlen(real_secret), + &secret, &secret_len); + + result = oath_totp_generate2(secret, + secret_len, + t, + time_step, + OATH_TOTP_DEFAULT_START_TIME, + digits, + 0, + otp); + + sscanf(otp, "%u", &code); + + oath_done(); + free(secret); + + if (code != [verificationCode unsignedIntValue]) + { + [self logWithFormat: @"Invalid Google Authenticator key for '%@'", username]; + json = [NSDictionary dictionaryWithObject: [NSNumber numberWithInt: 1] + forKey: @"GoogleAuthenticatorInvalidKey"]; + return [self responseWithStatus: 403 + andJSONRepresentation: json]; + } + } // if ([verificationCode length] == 6 && [verificationCode unsignedIntValue] > 0) + else + { + [self logWithFormat: @"Missing Google Authenticator key for '%@', asking it..", username]; + json = [NSDictionary dictionaryWithObject: [NSNumber numberWithInt: 1] + forKey: @"GoogleAuthenticatorMissingKey"]; + return [self responseWithStatus: 202 + andJSONRepresentation: json]; + } + } +#endif + json = [NSDictionary dictionaryWithObjectsAndKeys: [loggedInUser cn], @"cn", [NSNumber numberWithInt: expire], @"expire", @@ -265,8 +328,8 @@ } else { - [self logWithFormat:@"Login from '%@' for user '%@' might not have worked - password policy: %d grace: %d expire: %d bound: %d", - remoteHost, username, err, grace, expire, b]; + [self logWithFormat: @"Login from '%@' for user '%@' might not have worked - password policy: %d grace: %d expire: %d bound: %d", + remoteHost, username, err, grace, expire, b]; response = [self _responseWithLDAPPolicyError: err]; } @@ -639,4 +702,13 @@ return response; } +- (BOOL) isGoogleAuthenticatorEnabled +{ +#if defined(MFA_CONFIG) + return YES; +#else + return NO; +#endif +} + @end /* SOGoRootPage */ diff --git a/UI/PreferencesUI/UIxJSONPreferences.m b/UI/PreferencesUI/UIxJSONPreferences.m index ebfc34141..b058a3ba3 100644 --- a/UI/PreferencesUI/UIxJSONPreferences.m +++ b/UI/PreferencesUI/UIxJSONPreferences.m @@ -1,6 +1,6 @@ /* UIxJSONPreferences.m - this file is part of SOGo * - * Copyright (C) 2007-2017 Inverse inc. + * Copyright (C) 2007-2020 Inverse inc. * * 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 @@ -171,6 +171,9 @@ static SoProduct *preferencesProduct = nil; if (![[defaults source] objectForKey: @"SOGoAnimationMode"]) [[defaults source] setObject: [defaults animationMode] forKey: @"SOGoAnimationMode"]; + if (![[defaults source] objectForKey: @"SOGoGoogleAuthenticatorEnabled"]) + [[defaults source] setObject: [NSNumber numberWithBool: NO] forKey: @"SOGoGoogleAuthenticatorEnabled"]; + // // Default Calendar preferences // diff --git a/UI/PreferencesUI/UIxPreferences.m b/UI/PreferencesUI/UIxPreferences.m index 674b5d4b7..99acd901d 100644 --- a/UI/PreferencesUI/UIxPreferences.m +++ b/UI/PreferencesUI/UIxPreferences.m @@ -1,6 +1,6 @@ /* UIxPreferences.m - this file is part of SOGo * - * Copyright (C) 2007-2019 Inverse inc. + * Copyright (C) 2007-2020 Inverse inc. * * 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 @@ -1021,6 +1021,20 @@ static NSArray *reminderValues = nil; return [NSString stringWithString: SOGoVersion]; } +- (BOOL) isGoogleAuthenticatorEnabled +{ +#if defined(MFA_CONFIG) + return YES; +#else + return NO; +#endif +} + +- (NSString *) googleAuthenticatorKey +{ + return [[context activeUser] googleAuthenticatorKey]; +} + // // Used internally // diff --git a/UI/Templates/MainUI/SOGoRootPage.wox b/UI/Templates/MainUI/SOGoRootPage.wox index f86a5c88e..dc6babab3 100644 --- a/UI/Templates/MainUI/SOGoRootPage.wox +++ b/UI/Templates/MainUI/SOGoRootPage.wox @@ -52,6 +52,16 @@ + + + + email + + + +
language diff --git a/UI/Templates/PreferencesUI/UIxPreferences.wox b/UI/Templates/PreferencesUI/UIxPreferences.wox index 0ffe6a875..dbb2c7d8f 100644 --- a/UI/Templates/PreferencesUI/UIxPreferences.wox +++ b/UI/Templates/PreferencesUI/UIxPreferences.wox @@ -231,6 +231,27 @@ + + + + + + + +
diff --git a/UI/WebServerResources/js/Common/Authentication.service.js b/UI/WebServerResources/js/Common/Authentication.service.js index 343837fab..891d1ba19 100644 --- a/UI/WebServerResources/js/Common/Authentication.service.js +++ b/UI/WebServerResources/js/Common/Authentication.service.js @@ -62,6 +62,7 @@ var d = $q.defer(), username = data.username, password = data.password, + verificationCode = data.verificationCode, domain = data.domain, language, rememberLogin = data.rememberLogin ? 1 : 0; @@ -80,6 +81,7 @@ data: { userName: username, password: password, + verificationCode: verificationCode, domain: domain, language: language, rememberLogin: rememberLogin @@ -91,8 +93,12 @@ d.reject({error: l('cookiesNotEnabled')}); } else { + // Check for Google Authenticator 2FA + if (typeof data.GoogleAuthenticatorMissingKey != 'undefined' && response.status == 202) { + d.resolve({gamissingkey: 1}); + } // Check password policy - if (typeof data.expire != 'undefined' && typeof data.grace != 'undefined') { + else if (typeof data.expire != 'undefined' && typeof data.grace != 'undefined') { if (data.expire < 0 && data.grace > 0) { d.reject({grace: data.grace}); //showPasswordDialog('grace', createPasswordGraceDialog, data['grace']); @@ -110,7 +116,10 @@ } }, function(response) { var msg, perr, data = response.data; - if (data && data.LDAPPasswordPolicyError) { + if (data && data.GoogleAuthenticatorInvalidKey) { + msg = l('You provided an invalid Google Authenticator key.'); + } + else if (data && data.LDAPPasswordPolicyError) { perr = data.LDAPPasswordPolicyError; if (perr == passwordPolicyConfig.PolicyNoError) { msg = l('Wrong username or password.'); diff --git a/UI/WebServerResources/js/Main/Main.app.js b/UI/WebServerResources/js/Main/Main.app.js index 2bf9f9970..d4748d6e3 100644 --- a/UI/WebServerResources/js/Main/Main.app.js +++ b/UI/WebServerResources/js/Main/Main.app.js @@ -23,6 +23,7 @@ if (/\blanguage=/.test($window.location.search)) this.creds.language = $window.language; this.loginState = false; + this.showGoogleAuthenticatorCode = false; // Show login once everything is initialized this.showLogin = false; @@ -33,16 +34,23 @@ vm.loginState = 'authenticating'; Authentication.login(vm.creds) .then(function(data) { - vm.loginState = 'logged'; - vm.cn = data.cn; - // Let the user see the succesfull message before reloading the page - $timeout(function() { - if ($window.location.href === data.url) - $window.location.reload(true); - else - $window.location.href = data.url; - }, 1000); + if (typeof data.gamissingkey != 'undefined' && data.gamissingkey == 1) { + vm.showGoogleAuthenticatorCode = true; + vm.loginState = 'error'; + } + else { + vm.loginState = 'logged'; + vm.cn = data.cn; + + // Let the user see the succesfull message before reloading the page + $timeout(function() { + if ($window.location.href === data.url) + $window.location.reload(true); + else + $window.location.href = data.url; + }, 1000); + } }, function(msg) { vm.loginState = 'error'; vm.errorMessage = msg.error; diff --git a/configure b/configure index 999dc8f29..19dd92338 100755 --- a/configure +++ b/configure @@ -25,6 +25,7 @@ ARG_CFGSSL="auto" ARG_WITH_DEBUG=1 ARG_WITH_STRIP=0 ARG_ENABLE_SAML2=0 +ARG_ENABLE_MFA=0 ARG_WITH_LDAP_CONFIG=0 GNUSTEP_INSTALLATION_DOMAIN="LOCAL" @@ -76,6 +77,7 @@ Installation directories: --enable-strip turn on stripping of debug symbols --with-ssl=SSL specify ssl library (none, ssl, gnutls, auto) [auto] --enable-saml2 enable support for SAML2 authentication (requires liblasso) + --enable-mfa enable multi-factor authentication (requires liboath) --enable-ldap-config enable LDAP based configuration of SOGo @@ -106,6 +108,11 @@ printParas() { else echo " saml2 support: no"; fi + if test $ARG_ENABLE_MFA = 1; then + echo " mfa support: yes"; + else + echo " mfa support: no"; + fi if test $ARG_WITH_LDAP_CONFIG = 1; then echo " ldap-based configuration: yes"; else @@ -312,6 +319,11 @@ genConfigMake() { cfgwrite "saml2_config:=yes" fi + if test $ARG_ENABLE_MFA = 1; then + cfgwrite "ADDITIONAL_CPPFLAGS += -DMFA_CONFIG=1" + cfgwrite "mfa_config:=yes" + fi + if test $ARG_WITH_LDAP_CONFIG = 1; then cfgwrite "ADDITIONAL_CPPFLAGS += -DLDAP_CONFIG=1" cfgwrite "ldap_config:=yes" @@ -324,7 +336,7 @@ checkLinking() { # library-name => $1, type => $2 local oldpwd="${PWD}" local tmpdir=".configure-test-$$" - + mkdir $tmpdir cd $tmpdir cat > dummytool.c < +- added liboath requirements for RHELv7 + * Thu Mar 31 2015 Inverse inc. - Change script start sogod for systemd