feat(core): initial Google Authenticator support for 2FA

This commit is contained in:
Ludovic Marcotte
2020-05-07 07:22:24 -04:00
parent 33d3154d15
commit f78300a12e
17 changed files with 263 additions and 27 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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 <liboath/oath.h>
#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
{

View File

@@ -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;

View File

@@ -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"];

View File

@@ -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

View File

@@ -50,6 +50,10 @@
#import <SOGo/SOGoUserManager.h>
#import <SOGo/SOGoWebAuthenticator.h>
#if defined(MFA_CONFIG)
#include <liboath/oath.h>
#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 */

View File

@@ -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
//

View File

@@ -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
//

View File

@@ -52,6 +52,16 @@
<input type="password" ng-model="app.creds.password" ng-required="true"/>
</md-input-container>
<var:if condition="isGoogleAuthenticatorEnabled">
<md-input-container class="md-block"
ng-show="app.showGoogleAuthenticatorCode">
<label><var:string label:value="Google Authenticator
Verification Code"/></label>
<md-icon>email</md-icon>
<input type="text" ng-model="app.creds.verificationCode" ng-required="false"/>
</md-input-container>
</var:if>
<!-- LANGUAGES SELECT -->
<div layout="row" layout-align="start end">
<md-icon>language</md-icon>

View File

@@ -231,6 +231,27 @@
</md-radio-group>
</md-input-container>
<var:if condition="isGoogleAuthenticatorEnabled">
<md-checkbox flex="20"
ng-model="app.preferences.defaults.SOGoGoogleAuthenticatorEnabled"
ng-true-value="1"
ng-false-value="0"
label:aria-label="Enable two-factor authentication using Google Authenticator">
<var:string label:value="Enable two-factor authentication using Google Authenticator"/>
</md-checkbox>
<input type="text"
ng-readonly="true"
ng-show="app.preferences.defaults.SOGoGoogleAuthenticatorEnabled == 1"
var:value="googleAuthenticatorKey"/>
<label
ng-show="app.preferences.defaults.SOGoGoogleAuthenticatorEnabled
== 1"><var:string label:value="You must enter
this key into your Google Authenticator
application. If you do not and you log out
you will not be able to access SOGo
again."/></label>
</var:if>
</div>
</md-content>
</md-tab>

View File

@@ -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.');

View File

@@ -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;

24
configure vendored
View File

@@ -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 <<EOF
@@ -389,6 +401,12 @@ checkDependencies() {
cfgwrite "LASSO_LIBS := $lasso_libs"
fi;
fi
if test "x$ARG_ENABLE_MFA" = "x1"; then
checkLinking "oath" required;
if test $? = 0; then
cfgwrite "MFA_LIBS := -loath"
fi;
fi
if test "x$ARG_CFGSSL" = "xauto"; then
checkLinking "ssl" optional;
if test $? != 0; then
@@ -480,7 +498,9 @@ processOption() {
"x--enable-saml2")
ARG_ENABLE_SAML2=1
;;
"x--enable-mfa")
ARG_ENABLE_MFA=1
;;
"x--enable-ldap-config")
ARG_WITH_LDAP_CONFIG=1
;;

View File

@@ -7,12 +7,28 @@ DESTDIR=$(CURDIR)/debian/tmp
SAML2_CONFIG=--enable-saml2
#ifeq ($(DIST_CODENAME), stretch)
MFA_CONFIG=--enable-mfa
#endif
#ifeq ($(DIST_CODENAME), buster)
MFA_CONFIG=--enable-mfa
#endif
#ifeq ($(DIST_CODENAME), xenial)
MFA_CONFIG=--enable-mfa
#endif
#ifeq ($(DIST_CODENAME), bionic)
MFA_CONFIG=--enable-mfa
#endif
include /etc/GNUstep/GNUstep.conf
include /usr/share/GNUstep/Makefiles/common.make
config.make: configure
dh_testdir
./configure $(SAML2_CONFIG)
./configure $(SAML2_CONFIG) $(MFA_CONFIG)
#Architecture
build: build-arch

View File

@@ -34,11 +34,17 @@ BuildRequires: gcc-objc gnustep-base gnustep-make sope%{sope_major_version}%{so
# saml is enabled everywhere except on el5 since its glib2 is prehistoric
%define saml2_cfg_opts "--enable-saml2"
%define mfa_cfg_opts "--enable-mfa"
%{?el5:%define saml2_cfg_opts ""}
%{?el5:%define mfa_cfg_opts ""}
%{?el6:%define mfa_cfg_opts ""}
%{?el6:Requires: lasso}
%{?el6:BuildRequires: lasso-devel}
%{?el7:Requires: lasso}
%{?el7:BuildRequires: lasso-devel}
%{?el7:Requires: liboath}
%{?el7:BuildRequires: liboath-devel}
%description
SOGo is a groupware server built around OpenGroupware.org (OGo) and
@@ -155,7 +161,7 @@ rm -fr ${RPM_BUILD_ROOT}
%else
. /usr/share/GNUstep/Makefiles/GNUstep.sh
%endif
./configure %saml2_cfg_opts
./configure %saml2_cfg_opts %mfa_cfg_opts
case %{_target_platform} in
ppc64-*)
@@ -376,6 +382,9 @@ fi
# ********************************* changelog *************************
%changelog
* Thu Apr 30 2020 Inverse inc. <support@inverse.ca>
- added liboath requirements for RHELv7
* Thu Mar 31 2015 Inverse inc. <support@inverse.ca>
- Change script start sogod for systemd