Skip to content

Commit

Permalink
tweak(TB Login) enable fido2 style pwd less login
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmhh committed Jan 7, 2025
1 parent f9f2d7d commit bc2f1ff
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 5 deletions.
43 changes: 43 additions & 0 deletions tests/tine20/Tinebase/Auth/MFATest.php
Original file line number Diff line number Diff line change
Expand Up @@ -458,4 +458,47 @@ public function testGenericSmsAdapter()
$this->assertFalse($mfa->validate($sessionData['pin'], $this->_originalTestUser->mfa_configs->getFirstRecord()),
'validate didn\'t fail as expected on second call');
}

public function testWebAuthNPwdLessLogin(): void
{
$this->_originalTestUser->mfa_configs = new Tinebase_Record_RecordSet(
Tinebase_Model_MFA_UserConfig::class, [[
Tinebase_Model_MFA_UserConfig::FLD_ID => 'unittest',
Tinebase_Model_MFA_UserConfig::FLD_MFA_CONFIG_ID => 'unittest',
Tinebase_Model_MFA_UserConfig::FLD_CONFIG_CLASS =>
Tinebase_Auth_WebAuthnUserConfigMock::class,
Tinebase_Model_MFA_UserConfig::FLD_CONFIG =>
new Tinebase_Auth_WebAuthnUserConfigMock([
Tinebase_Model_MFA_WebAuthnUserConfig::FLD_WEBAUTHN_ID => 'unittest',
Tinebase_Model_MFA_WebAuthnUserConfig::FLD_PUBLIC_KEY_DATA => 'unittest',
]),
]]);
Tinebase_User::getInstance()->updateUserInSqlBackend($this->_originalTestUser);

$this->_createAreaLockConfig([], [
Tinebase_Model_MFA_Config::FLD_ID => 'unittest',
Tinebase_Model_MFA_Config::FLD_USER_CONFIG_CLASS =>
Tinebase_Auth_WebAuthnUserConfigMock::class,
Tinebase_Model_MFA_Config::FLD_PROVIDER_CONFIG_CLASS =>
Tinebase_Model_MFA_WebAuthnConfig::class,
Tinebase_Model_MFA_Config::FLD_PROVIDER_CLASS =>
Tinebase_Auth_WebAuthnAdapterMock::class,
Tinebase_Model_MFA_Config::FLD_PROVIDER_CONFIG => [
Tinebase_Model_MFA_WebAuthnConfig::FLD_RESIDENT_KEY_REQUIREMENT => 'https://shoo.tld/restapi/message',
],
Tinebase_Model_MFA_Config::FLD_ALLOW_PWD_LESS_LOGIN => true,
]);

if (Tinebase_AreaLock::getInstance()->hasLock(Tinebase_Model_AreaLockConfig::AREA_LOGIN)) {
Tinebase_AreaLock::getInstance()->forceUnlock(Tinebase_Model_AreaLockConfig::AREA_LOGIN);
}
try {
(new Tinebase_Frontend_Json())->login($this->_originalTestUser->accountLoginName);
$this->fail('except ' . Tinebase_Exception_AreaLocked::class . ' exception to be thrown');
} catch (Tinebase_Exception_AreaLocked $a) {

}
$result = (new Tinebase_Frontend_Json())->login($this->_originalTestUser->accountLoginName, null, 'unittest', '....');
$this->assertTrue($result['success']);
}
}
20 changes: 20 additions & 0 deletions tests/tine20/Tinebase/Auth/WebAuthnAdapterMock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types=1);

/**
* Tine 2.0
*
* @package Tinebase
* @subpackage Auth
* @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
* @copyright Copyright (c) 2024 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Paul Mehrer <p.mehrer@metaways.de>
*
*/

class Tinebase_Auth_WebAuthnAdapterMock extends Tinebase_Auth_MFA_WebAuthnAdapter
{
public function validate($_data, Tinebase_Model_MFA_UserConfig $_userCfg): bool
{
return true;
}
}
34 changes: 34 additions & 0 deletions tests/tine20/Tinebase/Auth/WebAuthnUserConfigMock.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php declare(strict_types=1);
/**
* Tine 2.0
*
* @package Tinebase
* @subpackage Auth
* @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
* @copyright Copyright (c) 2024 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Paul Mehrer <p.mehrer@metaways.de>
*/

/**
* WebAuthn MFA UserConfig Model
*
* @package Tinebase
* @subpackage Auth
*/
class Tinebase_Auth_WebAuthnUserConfigMock extends Tinebase_Model_MFA_WebAuthnUserConfig
{
public function updateUserNewRecordCallback(Tinebase_Model_FullUser $newUser, ?Tinebase_Model_FullUser $oldUser, Tinebase_Model_MFA_UserConfig $userCfg)
{
}

public function updateUserOldRecordCallback(Tinebase_Model_FullUser $newUser, Tinebase_Model_FullUser $oldUser, Tinebase_Model_MFA_UserConfig $userCfg)
{
}

/**
* holds the configuration object (must be declared in the concrete class)
*
* @var Tinebase_ModelConfiguration
*/
protected static $_configurationObject = NULL;
}
13 changes: 9 additions & 4 deletions tine20/Tinebase/Auth/MFA.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ public function validate($_data, Tinebase_Model_MFA_UserConfig $_userCfg): bool
}
}

public function getConfig(): Tinebase_Model_MFA_Config
{
return $this->_config;
}

public function getAdapter(): Tinebase_Auth_MFA_AdapterInterface
{
return $this->_adapter;
Expand Down Expand Up @@ -115,17 +120,17 @@ private function __construct(Tinebase_Model_MFA_Config $config)
$config->{Tinebase_Model_MFA_Config::FLD_PROVIDER_CONFIG},
$config->getId()
);
$this->_config = $config;
}

/**
* don't clone. Use the singleton.
*/
private function __clone() {}

/**
* @var Tinebase_Auth_MFA_AdapterInterface
*/
private $_adapter;
private Tinebase_Auth_MFA_AdapterInterface $_adapter;

private Tinebase_Model_MFA_Config $_config;

/**
* holds the instances of the singleton
Expand Down
51 changes: 51 additions & 0 deletions tine20/Tinebase/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,57 @@ protected function _validateAuthResult(Zend_Auth_Result $authResult, Tinebase_Mo
return $user;
}

public function passwordLessLogin(Tinebase_Model_FullUser $user, ?string $userMFACfgId, ?string $userMFApwd, string $clientIdString): bool
{
if (!$user->mfa_configs instanceof Tinebase_Record_RecordSet) {
return false;
}

if (null !== $userMFACfgId) {
if (!($mfaCfg = $user->mfa_configs->getById($userMFACfgId))) {
return false;
}
try {
/** @var Tinebase_Model_MFA_UserConfig $mfaCfg */
$mfa = Tinebase_Auth_MFA::getInstance($mfaCfg->{Tinebase_Model_MFA_UserConfig::FLD_MFA_CONFIG_ID});
} catch (Tinebase_Exception_Backend) {
return false;
}

if ($mfa->getConfig()->{Tinebase_Model_MFA_Config::FLD_ALLOW_PWD_LESS_LOGIN} && $mfa->validate($userMFApwd, $mfaCfg)) {
$authResult = new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $user->accountLoginName);
$accessLog = Tinebase_AccessLog::getInstance()->getAccessLogEntry($user->accountLoginName, $authResult, Tinebase_Core::get(Tinebase_Core::REQUEST),
$clientIdString);

Tinebase_AreaLock::getInstance()->forceUnlock(Tinebase_Model_AreaLockConfig::AREA_LOGIN);
$user = $this->_validateAuthResult($authResult, $accessLog);

if (!($user instanceof Tinebase_Model_FullUser)) {
return false;
}

$this->_loginUser($user, $accessLog);

return true;
}

//return false;
//we fall through and use the outer return false ... to make phpstan happy
} else {
$availableUserMFAcfgs = $user->mfa_configs->filter(fn (Tinebase_Model_MFA_UserConfig $uCfg) =>
Tinebase_Auth_MFA::getInstance($uCfg->{Tinebase_Model_MFA_UserConfig::FLD_MFA_CONFIG_ID})->getConfig()->{Tinebase_Model_MFA_Config::FLD_ALLOW_PWD_LESS_LOGIN});

if (0 === $availableUserMFAcfgs->count()) {
return false;
}

$this->_throwMFAException(new Tinebase_Model_AreaLockConfig([Tinebase_Model_AreaLockConfig::FLD_AREA_NAME => Tinebase_Model_AreaLockConfig::AREA_LOGIN], true),
$availableUserMFAcfgs, $user);
}

return false;
}

/**
* @param Tinebase_Model_AccessLog $accessLog
* @param Tinebase_Model_FullUser $user
Expand Down
9 changes: 9 additions & 0 deletions tine20/Tinebase/Frontend/Json.php
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,15 @@ public function login(?string $username = null, ?string $password = null, ?strin
if (SSO_Controller::passwordLessLogin($username)) {
return $this->_getLoginSuccessResponse($username);
}
$user = null;
try {
$user = Tinebase_User::getInstance()->getFullUserByLoginName($username);
} catch(Tinebase_Exception_NotFound) {}
if (null !== $user) {
if (Tinebase_Controller::getInstance()->passwordLessLogin($user, $MFAUserConfigId, $MFAPassword, self::REQUEST_TYPE)) {
return $this->_getLoginSuccessResponse($username);
}
}
throw new Tinebase_Exception_Auth_PwdRequired();
}

Expand Down
6 changes: 5 additions & 1 deletion tine20/Tinebase/Model/MFA/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* @package Tinebase
* @subpackage MFA
* @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
* @copyright Copyright (c) 2021-2022 Metaways Infosystems GmbH (http://www.metaways.de)
* @copyright Copyright (c) 2021-2024 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Paul Mehrer <p.mehrer@metaways.de>
*/

Expand All @@ -25,6 +25,7 @@ class Tinebase_Model_MFA_Config extends Tinebase_Record_NewAbstract
const FLD_PROVIDER_CONFIG = 'provider_config';
const FLD_USER_CONFIG_CLASS = 'user_config_class';
const FLD_ALLOW_SELF_SERVICE = 'allow_self_service';
const FLD_ALLOW_PWD_LESS_LOGIN = 'allow_pwd_less_login';

/**
* Holds the model configuration (must be assigned in the concrete class)
Expand Down Expand Up @@ -57,6 +58,9 @@ class Tinebase_Model_MFA_Config extends Tinebase_Record_NewAbstract
self::FLD_ALLOW_SELF_SERVICE => [
self::TYPE => self::TYPE_BOOLEAN,
],
self::FLD_ALLOW_PWD_LESS_LOGIN => [
self::TYPE => self::TYPE_BOOLEAN,
],
]
];

Expand Down

0 comments on commit bc2f1ff

Please sign in to comment.