mirror of
https://github.com/inverse-inc/sogo.git
synced 2026-09-21 05:08:32 +00:00
feat(core): initial Google Authenticator support for 2FA
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user