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
+4
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
+78 -6
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 */
+4 -1
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
//
+15 -1
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
//
+10
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>
@@ -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>
@@ -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.');
+17 -9
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;