Skip to content

Commit

Permalink
Merge branch '2024.11'
Browse files Browse the repository at this point in the history
  • Loading branch information
gitlabci committed Jan 10, 2025
2 parents 913c03f + 04a7ac7 commit c27d6ab
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 66 deletions.
2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -4567,7 +4567,7 @@ parameters:

-
message: "#^Call to an undefined method Tinebase_Auth_MFA_AdapterInterface\\:\\:getConfig\\(\\)\\.$#"
count: 2
count: 4
path: tine20/Tinebase/Frontend/Json.php

-
Expand Down
5 changes: 5 additions & 0 deletions tine20/Felamimail/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@ protected function _handleEvent(Tinebase_Event_Abstract $_eventObject)
break;
}
break;
case Tinebase_Event_User_Login::class:
if (null !== $_eventObject->password) {
$this->handleAccountLogin($_eventObject->user, $_eventObject->password);
}
break;
}
}

Expand Down
25 changes: 14 additions & 11 deletions tine20/Tinebase/Auth/Webauthn.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public static function webAuthnAuthenticate(Tinebase_Model_MFA_WebAuthnConfig $c
/** @var \Webauthn\PublicKeyCredentialSource $result */
$result = self::_getServer()->loadAndCheckAssertionResponse(
$data ?: $request->getBody()->getContents(),
self::getWebAuthnRequestOptions($config),
self::getWebAuthnRequestOptions($config, false),
null,
$request
);
Expand Down Expand Up @@ -95,25 +95,28 @@ public static function getWebAuthnCreationOptions(bool $createChallenge = false,
return $credentialCreationOptions;
}

public static function getWebAuthnRequestOptions(Tinebase_Model_MFA_WebAuthnConfig $config, ?string $accountId = null): \Webauthn\PublicKeyCredentialRequestOptions
public static function getWebAuthnRequestOptions(Tinebase_Model_MFA_WebAuthnConfig $config, bool $generateChallenge, ?string $accountId = null): \Webauthn\PublicKeyCredentialRequestOptions
{
if (null === $accountId) {
if (false === $generateChallenge) {
if (!($challenge = Tinebase_Session::getSessionNamespace(__CLASS__)->authchallenge)) {
throw new Tinebase_Exception_Backend('no authentication challenge found');
}
Tinebase_Session::getSessionNamespace(__CLASS__)->authchallenge = null;
$credentialRequestOptions = \Webauthn\PublicKeyCredentialRequestOptions::createFromString($challenge);
} else {
$user = Tinebase_User::getInstance()->getFullUserById($accountId);
$credDescriptors = [];
foreach ((new Tinebase_Auth_WebAuthnPublicKeyCredentialSourceRepository())->findAllForUserEntity(
new \Webauthn\PublicKeyCredentialUserEntity(
$user->accountLoginName, $user->getId(), $user->accountDisplayName
)) as $val) {
$credDescriptors[] = $val->getPublicKeyCredentialDescriptor();
}
$clientInputs = new AuthenticationExtensionsClientInputs();
$clientInputs->add(new AuthenticationExtension('userHandle', $user->getId()));
if (null !== $accountId) {
$user = Tinebase_User::getInstance()->getFullUserById($accountId);
$clientInputs->add(new AuthenticationExtension('userHandle', $user->getId()));
foreach ((new Tinebase_Auth_WebAuthnPublicKeyCredentialSourceRepository())->findAllForUserEntity(
new \Webauthn\PublicKeyCredentialUserEntity(
$user->accountLoginName, $user->getId(), $user->accountDisplayName
)) as $val) {
$credDescriptors[] = $val->getPublicKeyCredentialDescriptor();
}
}

$credentialRequestOptions = self::_getServer()->generatePublicKeyCredentialRequestOptions(
$config->{Tinebase_Model_MFA_WebAuthnConfig::FLD_USER_VERIFICATION_REQUIREMENT},
$credDescriptors,
Expand Down
60 changes: 29 additions & 31 deletions tine20/Tinebase/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@ public static function getInstance()
*
* @param string $loginName
* @param string $password
* @param \Zend\Http\PhpEnvironment\Request $request
* @param \Laminas\Http\PhpEnvironment\Request $request
* @param string $clientIdString
*
* @return bool
* @throws Tinebase_Exception_MaintenanceMode
*/
public function login($loginName, $password, \Zend\Http\PhpEnvironment\Request $request, $clientIdString = NULL)
public function login($loginName, $password, \Laminas\Http\PhpEnvironment\Request $request, $clientIdString = NULL)
{
// enforce utf8
$password = Tinebase_Helper::mbConvertTo($password);
Expand All @@ -114,13 +114,23 @@ public function login($loginName, $password, \Zend\Http\PhpEnvironment\Request $

// rolechange user: username*authuser?
$authUserParts = preg_split('/\*+(?=[^*]+$)/', $loginName);
$roleChangeUserName = null;
if (isset($authUserParts[1]) && Tinebase_User::getInstance()->getUserByLoginName($authUserParts[1])) {
$loginName = $authUserParts[1];
$roleChangeUserName = $authUserParts[0];
}

$authResult = Tinebase_Auth::getInstance()->authenticate($loginName, $password);

return $this->processLoginAuthResult($loginName, $request, $password, $clientIdString, $authResult, $roleChangeUserName);
}

public function processLoginAuthResult(string $loginName, \Laminas\Http\PhpEnvironment\Request $request, ?string $password = null, ?string $clientIdString = null, ?Zend_Auth_Result $authResult = null, ?string $roleChangeUserName = null): bool
{
if (null === $authResult) {
$authResult = new Zend_Auth_Result(Zend_Auth_Result::SUCCESS, $loginName);
}

$accessLog = Tinebase_AccessLog::getInstance()->getAccessLogEntry($loginName, $authResult, $request,
$clientIdString);

Expand All @@ -132,29 +142,26 @@ public function login($loginName, $password, \Zend\Http\PhpEnvironment\Request $

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

$this->_checkPasswordPolicyAtLogin($password, $user);
if (null !== $password) {
$this->_checkPasswordPolicyAtLogin($password, $user);

if (Tinebase_Config::getInstance()->{Tinebase_Config::PASSWORD_NTLMV2_HASH_UPDATE_ON_LOGIN}) {
$userController = Tinebase_User::getInstance();
if ($userController instanceof Tinebase_User_Sql) {
$userController->updateNtlmV2Hash($user->getId(), $password);
if (Tinebase_Config::getInstance()->{Tinebase_Config::PASSWORD_NTLMV2_HASH_UPDATE_ON_LOGIN}) {
$userController = Tinebase_User::getInstance();
if ($userController instanceof Tinebase_User_Sql) {
$userController->updateNtlmV2Hash($user->getId(), $password);
}
}
}

// TODO generalize this -> apps should be able to react to "user login" event
if (Tinebase_Application::getInstance()->isInstalled('Felamimail', true)) {
Felamimail_Controller::getInstance()->handleAccountLogin($user, $password);
}

if (isset($roleChangeUserName)) {
Tinebase_Controller::getInstance()->changeUserAccount($roleChangeUserName);
}

$loginEvent = new Tinebase_Event_User_Login();
$loginEvent->password = $password;
$loginEvent->user = $user;
Tinebase_Event::fireEvent($loginEvent);

if (null !== $roleChangeUserName) {
Tinebase_Controller::getInstance()->changeUserAccount($roleChangeUserName);
}

return true;
}

Expand Down Expand Up @@ -774,7 +781,7 @@ protected function _handleEvent(Tinebase_Event_Abstract $_eventObject)
break;

case Tinebase_Event_User_Login::class:
if (($userCtrl = Tinebase_User::getInstance()) instanceof Tinebase_User_Interface_SyncAble
if (null !== $_eventObject->password && ($userCtrl = Tinebase_User::getInstance()) instanceof Tinebase_User_Interface_SyncAble
&& Tinebase_Config::getInstance()->{Tinebase_Config::USERBACKEND}->{Tinebase_Config::SYNCOPTIONS}->{Tinebase_Config::SYNC_USER_OF_GROUPS}
&& !$userCtrl->isReadOnlyUser($_eventObject->user->getId())) {
$userCtrl->setPasswordInSyncBackend($_eventObject->user, $_eventObject->password);
Expand Down Expand Up @@ -889,20 +896,11 @@ public function passwordLessLogin(Tinebase_Model_FullUser $user, ?string $userMF
}

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 $this->processLoginAuthResult(
loginName: $user->accountLoginName,
request: Tinebase_Core::get(Tinebase_Core::REQUEST),
clientIdString: $clientIdString
);
}

//return false;
Expand Down
8 changes: 4 additions & 4 deletions tine20/Tinebase/Core.php
Original file line number Diff line number Diff line change
Expand Up @@ -1843,10 +1843,10 @@ public static function getCoreRegistryData(): array
'licenseStatus' => Tinebase_License::getInstance()->getStatus(),
'licenseData' => Tinebase_License::getInstance()->getCertificateData(),
'loginExternalIdps' => SSO_Controller_ExternalIdp::getInstance()->search(
Tinebase_Model_Filter_FilterGroup::getFilterForModel(SSO_Model_ExternalIdp::class, [
[TMFA::FIELD => SSO_Model_ExternalIdp::FLD_SHOW_AS_LOGIN_OPTION, TMFA::OPERATOR => TMFA::OP_EQUALS, TMFA::VALUE => true],
])
)->toArray(),
Tinebase_Model_Filter_FilterGroup::getFilterForModel(SSO_Model_ExternalIdp::class, [
[TMFA::FIELD => SSO_Model_ExternalIdp::FLD_SHOW_AS_LOGIN_OPTION, TMFA::OPERATOR => TMFA::OP_EQUALS, TMFA::VALUE => true],
])
)->toArray(),
];

if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
Expand Down
4 changes: 2 additions & 2 deletions tine20/Tinebase/Event/User/Login.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* @package Tinebase
* @subpackage Event
* @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
* @copyright Copyright (c) 2024 Metaways Infosystems GmbH (http://www.metaways.de)
* @copyright Copyright (c) 2024-2025 Metaways Infosystems GmbH (http://www.metaways.de)
* @author Paul Mehrer <[email protected]>
*/

Expand All @@ -18,5 +18,5 @@
class Tinebase_Event_User_Login extends Tinebase_Event_Abstract
{
public Tinebase_Model_FullUser $user;
public string $password;
public ?string $password;
}
62 changes: 49 additions & 13 deletions tine20/Tinebase/Frontend/Json.php
Original file line number Diff line number Diff line change
Expand Up @@ -586,17 +586,6 @@ public function authenticate($username, $password)
*/
public function login(?string $username = null, ?string $password = null, ?string $MFAUserConfigId = null, ?string $MFAPassword = null): array
{
if (empty($username)) {
try {
if (($idpId = ($this->_getRequestContextHeaders()['idpid'] ?? null)) &&
($idp = SSO_Controller_ExternalIdp::getInstance()->get($idpId)) &&
$idp->{SSO_Model_ExternalIdp::FLD_SHOW_AS_LOGIN_OPTION}) {
SSO_Controller::startExternalIdpAuthProcess($idp);
}
} catch (Tinebase_Exception_NotFound) {}
return $this->_getLoginFailedResponse();
}

try {
Tinebase_Core::startCoreSession();
} catch (Zend_Session_Exception $zse) {
Expand All @@ -609,6 +598,38 @@ public function login(?string $username = null, ?string $password = null, ?strin
);
}

if (empty($username)) {
try {
if (($idpId = ($this->_getRequestContextHeaders()['idpid'] ?? null)) &&
($idp = SSO_Controller_ExternalIdp::getInstance()->get($idpId)) &&
$idp->{SSO_Model_ExternalIdp::FLD_SHOW_AS_LOGIN_OPTION}) {
SSO_Controller::startExternalIdpAuthProcess($idp);
}
} catch (Tinebase_Exception_NotFound) {}
if ($MFAPassword) {
if (null !== $MFAUserConfigId ||
null !== ($MFAUserConfigId = Tinebase_Config::getInstance()->{Tinebase_Config::MFA}->records?->find(Tinebase_Model_MFA_Config::FLD_PROVIDER_CLASS, Tinebase_Auth_MFA_WebAuthnAdapter::class)?->getId())) {
try {
$user = Tinebase_Auth_Webauthn::webAuthnAuthenticate(Tinebase_Auth_MFA::getInstance($MFAUserConfigId)->getAdapter()->getConfig(), $MFAPassword);
} catch (Throwable) {
return $this->_getLoginFailedResponse();
}
$areaLock = Tinebase_AreaLock::getInstance();
if ($areaLock->hasLock(Tinebase_Model_AreaLockConfig::AREA_LOGIN)) {
$areaLock->forceUnlock(Tinebase_Model_AreaLockConfig::AREA_LOGIN);
}
if (Tinebase_Controller::getInstance()->processLoginAuthResult(
loginName: $user->accountLoginName,
request: Tinebase_Core::get(Tinebase_Core::REQUEST),
clientIdString: self::REQUEST_TYPE
)) {
return $this->_getLoginSuccessResponse($username);
}
}
}
return $this->_getLoginFailedResponse();
}

if (empty($password)) {
if (SSO_Controller::passwordLessLogin($username)) {
return $this->_getLoginSuccessResponse($username);
Expand Down Expand Up @@ -1515,7 +1536,22 @@ function ($value) {
return $result;
}

/**
public function getWebAuthnAuthenticateOptionsForLogin(?string $mfaId = null): array
{
if (null === $mfaId) {
if (null === ($mfaId = Tinebase_Config::getInstance()->{Tinebase_Config::MFA}->records?->find(Tinebase_Model_MFA_Config::FLD_PROVIDER_CLASS, Tinebase_Auth_MFA_WebAuthnAdapter::class)?->getId())) {
throw new Tinebase_Exception_Backend('service not configured');
}
}

return Tinebase_Auth_Webauthn::getWebAuthnRequestOptions(
Tinebase_Auth_MFA::getInstance($mfaId)->getAdapter()->getConfig(),
true
)->jsonSerialize();
}


/**
* @param string $accountLoginName
* @param string $mfaId
* @return array
Expand All @@ -1538,7 +1574,7 @@ public function getWebAuthnAuthenticateOptionsForMFA(string $accountLoginName, s
/** @var Tinebase_Model_MFA_WebAuthnConfig $config */
$config = Tinebase_Auth_MFA::getInstance($configId)->getAdapter()->getConfig();

return Tinebase_Auth_Webauthn::getWebAuthnRequestOptions($config, $account->getId())->jsonSerialize();
return Tinebase_Auth_Webauthn::getWebAuthnRequestOptions($config, true, $account->getId())->jsonSerialize();
}

public function getWebAuthnRegisterPublicKeyOptionsForMFA(string $mfaId, ?string $accountId = null)
Expand Down
3 changes: 2 additions & 1 deletion tine20/Tinebase/Server/Json.php
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,8 @@ protected function _checkJsonKey($method, $jsonKey)
'Tinebase.setLocale',
'Tinebase.checkAuthToken',
'Tinebase_AreaLock.unlock',
'Tinebase.getWebAuthnAuthenticateOptionsForMFA'
'Tinebase.getWebAuthnAuthenticateOptionsForMFA',
'Tinebase.getWebAuthnAuthenticateOptionsForLogin',
);

// check json key for all methods but some exceptions
Expand Down
8 changes: 5 additions & 3 deletions tine20/Tinebase/Server/WebDAV.php
Original file line number Diff line number Diff line change
Expand Up @@ -126,13 +126,15 @@ public function handle(\Laminas\Http\Request $request = null, $body = null)

if (Tinebase_Auth_NtlmV2::AUTH_SUCCESS === $ntlmAuthStatus) {
try {
Tinebase_Controller::getInstance()->loginUser($this->_ntlmV2->getUser(), $this->_request,
self::REQUEST_TYPE);
$hasIdentity = Tinebase_Controller::getInstance()->processLoginAuthResult(
loginName: $this->_ntlmV2->getUser()->accountLoginName,
request: $this->_request,
clientIdString: self::REQUEST_TYPE
);
} catch (Tinebase_Exception_MaintenanceMode $temm) {
header('HTTP/1.1 503 Service Unavailable');
return;
}
$hasIdentity = true;
} else {
$this->_ntlmV2->sendHeaderForAuthPase($ntlmAuthStatus);
return;
Expand Down

0 comments on commit c27d6ab

Please sign in to comment.