diff --git a/app/modules/web/Controllers/Bootstrap/GetEnvironmentController.php b/app/modules/web/Controllers/Bootstrap/GetEnvironmentController.php index ecbc693b..0abf14db 100644 --- a/app/modules/web/Controllers/Bootstrap/GetEnvironmentController.php +++ b/app/modules/web/Controllers/Bootstrap/GetEnvironmentController.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2024, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -36,7 +36,7 @@ use SP\Modules\Web\Controllers\SimpleControllerBase; use SP\Modules\Web\Controllers\Traits\JsonTrait; use SP\Mvc\Controller\SimpleControllerHelper; use SP\Plugin\PluginManager; -use SP\Providers\Auth\Browser\BrowserAuthInterface; +use SP\Providers\Auth\Browser\BrowserAuthService; /** * Class GetEnvironmentController @@ -48,15 +48,15 @@ final class GetEnvironmentController extends SimpleControllerBase use JsonTrait; private CryptPKIInterface $cryptPKI; - private PluginManager $pluginManager; - private BrowserAuthInterface $browser; + private PluginManager $pluginManager; + private BrowserAuthService $browser; public function __construct( Application $application, SimpleControllerHelper $simpleControllerHelper, CryptPKIInterface $cryptPKI, PluginManager $pluginManager, - BrowserAuthInterface $browser + BrowserAuthService $browser ) { $this->cryptPKI = $cryptPKI; $this->pluginManager = $pluginManager; diff --git a/app/modules/web/Controllers/ControllerBase.php b/app/modules/web/Controllers/ControllerBase.php index c44493a6..6253ac34 100644 --- a/app/modules/web/Controllers/ControllerBase.php +++ b/app/modules/web/Controllers/ControllerBase.php @@ -48,7 +48,7 @@ use SP\Modules\Web\Controllers\Helpers\LayoutHelper; use SP\Modules\Web\Controllers\Traits\WebControllerTrait; use SP\Mvc\Controller\WebControllerHelper; use SP\Mvc\View\TemplateInterface; -use SP\Providers\Auth\Browser\BrowserAuthInterface; +use SP\Providers\Auth\Browser\BrowserAuthService; use function SP\__; use function SP\logger; @@ -76,8 +76,8 @@ abstract class ControllerBase protected ?ProfileData $userProfileData = null; protected bool $isAjax; protected LayoutHelper $layoutHelper; - protected string $actionName; - private BrowserAuthInterface $browser; + protected string $actionName; + private BrowserAuthService $browser; public function __construct( Application $application, diff --git a/app/modules/web/Controllers/Login/LoginController.php b/app/modules/web/Controllers/Login/LoginController.php index 62b9c4db..5bcb507e 100644 --- a/app/modules/web/Controllers/Login/LoginController.php +++ b/app/modules/web/Controllers/Login/LoginController.php @@ -68,9 +68,8 @@ final class LoginController extends ControllerBase { try { $from = $this->getSignedUriFromRequest($this->request, $this->configData); - $this->loginService->setFrom($from); - $loginResponse = $this->loginService->doLogin(); + $loginResponse = $this->loginService->doLogin($from); $this->checkForwarded(); @@ -91,8 +90,10 @@ final class LoginController extends ControllerBase ); return $this->returnJsonResponseData([ - 'url' => $this->session->getTrasientKey('redirect') ?: $loginResponse->getRedirect(), - ]); + 'url' => $this->session->getTrasientKey( + 'redirect' + ) ?: $loginResponse->getRedirect(), + ]); } catch (Exception $e) { processException($e); diff --git a/lib/SP/Core/Definitions/CoreDefinitions.php b/lib/SP/Core/Definitions/CoreDefinitions.php index 8d1171c2..5ad78e4f 100644 --- a/lib/SP/Core/Definitions/CoreDefinitions.php +++ b/lib/SP/Core/Definitions/CoreDefinitions.php @@ -91,10 +91,10 @@ use SP\Mvc\View\Template; use SP\Mvc\View\TemplateInterface; use SP\Providers\Acl\AclHandler; use SP\Providers\Auth\AuthProvider; -use SP\Providers\Auth\AuthProviderInterface; -use SP\Providers\Auth\AuthTypeEnum; +use SP\Providers\Auth\AuthProviderService; +use SP\Providers\Auth\AuthType; use SP\Providers\Auth\Browser\BrowserAuth; -use SP\Providers\Auth\Browser\BrowserAuthInterface; +use SP\Providers\Auth\Browser\BrowserAuthService; use SP\Providers\Auth\Database\DatabaseAuth; use SP\Providers\Auth\Database\DatabaseAuthService; use SP\Providers\Auth\Ldap\LdapActions; @@ -167,7 +167,7 @@ final class CoreDefinitions ThemeInterface::class => autowire(Theme::class), TemplateInterface::class => autowire(Template::class), DatabaseAuthService::class => autowire(DatabaseAuth::class), - BrowserAuthInterface::class => autowire(BrowserAuth::class), + BrowserAuthService::class => autowire(BrowserAuth::class), LdapParams::class => factory([LdapParams::class, 'getFrom']), LdapConnectionInterface::class => autowire(LdapConnection::class), LdapActionsService::class => autowire(LdapActions::class), @@ -176,23 +176,23 @@ final class CoreDefinitions 'ldap', factory([LdapBase::class, 'factory']) ), - AuthProviderInterface::class => factory( + AuthProviderService::class => factory( static function ( - AuthProvider $authProvider, - ConfigDataInterface $configData, - LdapAuthService $ldapAuth, - BrowserAuthInterface $browserAuth, - DatabaseAuthService $databaseAuth, + AuthProvider $authProvider, + ConfigDataInterface $configData, + LdapAuthService $ldapAuth, + BrowserAuthService $browserAuth, + DatabaseAuthService $databaseAuth, ) { if ($configData->isLdapEnabled()) { - $authProvider->registerAuth($ldapAuth, AuthTypeEnum::Ldap); + $authProvider->registerAuth($ldapAuth, AuthType::Ldap); } if ($configData->isAuthBasicEnabled()) { - $authProvider->registerAuth($browserAuth, AuthTypeEnum::Browser); + $authProvider->registerAuth($browserAuth, AuthType::Browser); } - $authProvider->registerAuth($databaseAuth, AuthTypeEnum::Database); + $authProvider->registerAuth($databaseAuth, AuthType::Database); return $authProvider; } diff --git a/lib/SP/DataModel/ProfileData.php b/lib/SP/DataModel/ProfileData.php index 782850ef..76b3c876 100644 --- a/lib/SP/DataModel/ProfileData.php +++ b/lib/SP/DataModel/ProfileData.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2024, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -28,8 +28,6 @@ use SP\Domain\Common\Models\Model; /** * Class ProfileData - * - * @package SP\DataModel */ class ProfileData extends Model { diff --git a/lib/SP/Domain/Auth/Dtos/LoginResponseDto.php b/lib/SP/Domain/Auth/Dtos/LoginResponseDto.php index 2f7b767d..12463734 100644 --- a/lib/SP/Domain/Auth/Dtos/LoginResponseDto.php +++ b/lib/SP/Domain/Auth/Dtos/LoginResponseDto.php @@ -24,29 +24,18 @@ namespace SP\Domain\Auth\Dtos; -/** - * Class LoginResponse - * - * @package SP\Domain\Auth\Services - */ -final class LoginResponseDto -{ - private int $status; - private ?string $redirect; +use SP\Domain\Auth\Services\LoginStatus; - /** - * LoginResponse constructor. - * - * @param int $status - * @param string|null $redirect - */ - public function __construct(int $status, ?string $redirect = null) +/** + * Class LoginResponseDto + */ +final readonly class LoginResponseDto +{ + public function __construct(private LoginStatus $status, private ?string $redirect = null) { - $this->status = $status; - $this->redirect = $redirect; } - public function getStatus(): int + public function getStatus(): LoginStatus { return $this->status; } diff --git a/lib/SP/Domain/Auth/Dtos/UserLoginDto.php b/lib/SP/Domain/Auth/Dtos/UserLoginDto.php index a9d3f1a9..99ea10f0 100644 --- a/lib/SP/Domain/Auth/Dtos/UserLoginDto.php +++ b/lib/SP/Domain/Auth/Dtos/UserLoginDto.php @@ -24,27 +24,18 @@ namespace SP\Domain\Auth\Dtos; -use SP\Domain\User\Dtos\UserDataDto; - /** * Class UserLoginDto */ class UserLoginDto { - protected ?string $loginUser = null; - protected ?string $loginPass = null; - protected ?UserDataDto $userDataDto = null; + private ?string $loginUser; + private ?string $loginPass; - /** - * @param string|null $loginUser - * @param string|null $loginPass - * @param UserDataDto|null $userDataDto - */ - public function __construct(?string $loginUser = null, ?string $loginPass = null, ?UserDataDto $userDataDto = null) + public function __construct(?string $loginUser = null, ?string $loginPass = null) { $this->loginUser = $loginUser; $this->loginPass = $loginPass; - $this->userDataDto = $userDataDto; } public function getLoginUser(): ?string @@ -66,14 +57,4 @@ class UserLoginDto { $this->loginPass = $loginPass; } - - public function getUserDataDto(): ?UserDataDto - { - return $this->userDataDto; - } - - public function setUserDataDto(UserDataDto $userDataDto = null): void - { - $this->userDataDto = $userDataDto; - } } diff --git a/lib/SP/Domain/Auth/Ports/LdapAuthService.php b/lib/SP/Domain/Auth/Ports/LdapAuthService.php index 12c4972c..c3005173 100644 --- a/lib/SP/Domain/Auth/Ports/LdapAuthService.php +++ b/lib/SP/Domain/Auth/Ports/LdapAuthService.php @@ -24,15 +24,15 @@ namespace SP\Domain\Auth\Ports; -use SP\Providers\Auth\AuthInterface; +use SP\Providers\Auth\AuthService; use SP\Providers\Auth\Ldap\LdapAuthData; /** * Class LdapBase * - * @extends AuthInterface + * @extends AuthService */ -interface LdapAuthService extends AuthInterface +interface LdapAuthService extends AuthService { public const ACCOUNT_NO_GROUPS = 702; public const ACCOUNT_EXPIRED = 701; diff --git a/lib/SP/Domain/Auth/Ports/LoginAuthHandlerService.php b/lib/SP/Domain/Auth/Ports/LoginAuthHandlerService.php new file mode 100644 index 00000000..8b949fc5 --- /dev/null +++ b/lib/SP/Domain/Auth/Ports/LoginAuthHandlerService.php @@ -0,0 +1,64 @@ +. + */ + +namespace SP\Domain\Auth\Ports; + +use SP\Domain\Auth\Dtos\UserLoginDto; +use SP\Domain\Auth\Services\AuthException; +use SP\Providers\Auth\Browser\BrowserAuthData; +use SP\Providers\Auth\Database\DatabaseAuthData; +use SP\Providers\Auth\Ldap\LdapAuthData; + +/** + * Class LoginDatabase + */ +interface LoginAuthHandlerService +{ + /** + * Check response from database authentication + * + * @param DatabaseAuthData $authData + * @param UserLoginDto $userLoginDto + * @throws AuthException + */ + public function authDatabase(DatabaseAuthData $authData, UserLoginDto $userLoginDto): void; + + /** + * Check response from browser authentication + * + * @param BrowserAuthData $authData + * @param UserLoginDto $userLoginDto + * @throws AuthException + */ + public function authBrowser(BrowserAuthData $authData, UserLoginDto $userLoginDto): void; + + /** + * Check response from LDAP authentication + * + * @param LdapAuthData $authData + * @param UserLoginDto $userLoginDto + * @throws AuthException + */ + public function authLdap(LdapAuthData $authData, UserLoginDto $userLoginDto): void; +} diff --git a/lib/SP/Domain/Auth/Ports/LoginMasterPassService.php b/lib/SP/Domain/Auth/Ports/LoginMasterPassService.php new file mode 100644 index 00000000..a18eca0f --- /dev/null +++ b/lib/SP/Domain/Auth/Ports/LoginMasterPassService.php @@ -0,0 +1,46 @@ +. + */ + +namespace SP\Domain\Auth\Ports; + +use SP\Domain\Auth\Dtos\UserLoginDto; +use SP\Domain\Auth\Services\AuthException; +use SP\Domain\Common\Services\ServiceException; +use SP\Domain\User\Dtos\UserDataDto; + +/** + * Class LoginMasterPass + */ +interface LoginMasterPassService +{ + /** + * Load master password or request it + * + * @param UserLoginDto $userLoginDto + * @param UserDataDto $userDataDto + * @throws AuthException + * @throws ServiceException + */ + public function loadMasterPass(UserLoginDto $userLoginDto, UserDataDto $userDataDto): void; +} diff --git a/lib/SP/Domain/Auth/Ports/LoginService.php b/lib/SP/Domain/Auth/Ports/LoginService.php index 343ad264..399e5e1b 100644 --- a/lib/SP/Domain/Auth/Ports/LoginService.php +++ b/lib/SP/Domain/Auth/Ports/LoginService.php @@ -24,40 +24,32 @@ namespace SP\Domain\Auth\Ports; -use Defuse\Crypto\Exception\EnvironmentIsBrokenException; -use Exception; use SP\Domain\Auth\Dtos\LoginResponseDto; use SP\Domain\Auth\Services\AuthException; -use SP\Domain\Core\Exceptions\ConstraintException; -use SP\Domain\Core\Exceptions\QueryException; -use SP\Domain\Core\Exceptions\SPException; +use SP\Providers\Auth\AuthResult; /** - * Class LoginService - * - * @package SP\Domain\Common\Services + * Interface LoginService */ interface LoginService { /** - * Ejecutar las acciones de login + * Execute login process * + * @param string|null $from Set the source routable action * @return LoginResponseDto * @throws AuthException - * @throws SPException - * @throws EnvironmentIsBrokenException - * @throws ConstraintException - * @throws QueryException - * @throws Exception - * @uses Login::authBrowser() - * @uses Login::authDatabase() - * @uses Login::authLdap() - * */ - public function doLogin(): LoginResponseDto; + public function doLogin(?string $from = null): LoginResponseDto; /** - * @param string|null $from + * Handle the authentication result to determine whether the login is successful + * + * @param AuthResult $authResult The authentication result + * @throws AuthException + * @uses LoginAuthHandlerService::authBrowser() + * @uses LoginAuthHandlerService::authDatabase() + * @uses LoginAuthHandlerService::authLdap() */ - public function setFrom(?string $from): void; + public function handleAuthResponse(AuthResult $authResult): void; } diff --git a/lib/SP/Domain/Auth/Ports/LoginUserService.php b/lib/SP/Domain/Auth/Ports/LoginUserService.php new file mode 100644 index 00000000..ba2af5a8 --- /dev/null +++ b/lib/SP/Domain/Auth/Ports/LoginUserService.php @@ -0,0 +1,46 @@ +. + */ + +namespace SP\Domain\Auth\Ports; + +use SP\Domain\Auth\Dtos\LoginResponseDto; +use SP\Domain\Auth\Services\AuthException; +use SP\Domain\Common\Services\ServiceException; +use SP\Domain\User\Dtos\UserDataDto; + +/** + * Interface LoginUserService + */ +interface LoginUserService +{ + /** + * Check the user status + * + * @param UserDataDto $userDataDto + * @return LoginResponseDto + * @throws AuthException + * @throws ServiceException + */ + public function checkUser(UserDataDto $userDataDto): LoginResponseDto; +} diff --git a/lib/SP/Domain/Auth/Services/Login.php b/lib/SP/Domain/Auth/Services/Login.php index 4d563313..e4feff9c 100644 --- a/lib/SP/Domain/Auth/Services/Login.php +++ b/lib/SP/Domain/Auth/Services/Login.php @@ -24,425 +24,137 @@ namespace SP\Domain\Auth\Services; -use Defuse\Crypto\Exception\EnvironmentIsBrokenException; -use Exception; use SP\Core\Application; use SP\Core\Events\Event; use SP\Core\Events\EventMessage; use SP\DataModel\ProfileData; use SP\Domain\Auth\Dtos\LoginResponseDto; use SP\Domain\Auth\Dtos\UserLoginDto; -use SP\Domain\Auth\Ports\LdapAuthService; +use SP\Domain\Auth\Ports\LoginAuthHandlerService; +use SP\Domain\Auth\Ports\LoginMasterPassService; use SP\Domain\Auth\Ports\LoginService; -use SP\Domain\Common\Services\Service; +use SP\Domain\Auth\Ports\LoginUserService; use SP\Domain\Common\Services\ServiceException; -use SP\Domain\Config\Ports\ConfigDataInterface; use SP\Domain\Core\Exceptions\ConstraintException; -use SP\Domain\Core\Exceptions\CryptException; use SP\Domain\Core\Exceptions\InvalidArgumentException; use SP\Domain\Core\Exceptions\QueryException; -use SP\Domain\Core\Exceptions\SPException; use SP\Domain\Core\LanguageInterface; -use SP\Domain\Crypt\Ports\TemporaryMasterPassService; use SP\Domain\Http\RequestInterface; -use SP\Domain\Security\Dtos\TrackRequest; use SP\Domain\Security\Ports\TrackService; -use SP\Domain\User\Dtos\UserLoginRequest; -use SP\Domain\User\Ports\UserMasterPassService; -use SP\Domain\User\Ports\UserPassRecoverService; +use SP\Domain\User\Dtos\UserDataDto; use SP\Domain\User\Ports\UserProfileService; use SP\Domain\User\Ports\UserService; -use SP\Domain\User\Services\UserMasterPassStatus; -use SP\Http\Uri; use SP\Infrastructure\Common\Repositories\NoSuchItemException; -use SP\Providers\Auth\AuthProviderInterface; -use SP\Providers\Auth\Browser\BrowserAuthData; -use SP\Providers\Auth\Database\DatabaseAuthData; -use SP\Providers\Auth\Ldap\LdapAuthData; -use SP\Providers\Auth\Ldap\LdapCodeEnum; -use SP\Util\PasswordUtil; +use SP\Providers\Auth\AuthProviderService; +use SP\Providers\Auth\AuthResult; -use function SP\__; use function SP\__u; /** * Class Login */ -final class Login extends Service implements LoginService +final class Login extends LoginBase implements LoginService { - private const STATUS_INVALID_LOGIN = 1; - private const STATUS_INVALID_MASTER_PASS = 2; - private const STATUS_USER_DISABLED = 3; - private const STATUS_NEED_OLD_PASS = 5; - private const STATUS_MAX_ATTEMPTS_EXCEEDED = 6; - private const STATUS_PASS_RESET = 7; - private const STATUS_PASS = 0; - private const STATUS_NONE = 100; - - private UserLoginDto $userLoginData; - private ConfigDataInterface $configData; - private TrackRequest $trackRequest; - private ?string $from = null; + private readonly UserLoginDto $userLoginDto; /** * @throws InvalidArgumentException */ public function __construct( - Application $application, - private readonly AuthProviderInterface $authProvider, - private readonly LanguageInterface $language, - private readonly TrackService $trackService, - private readonly RequestInterface $request, - private readonly UserService $userService, - private readonly UserPassRecoverService $userPassRecoverService, - private readonly TemporaryMasterPassService $temporaryMasterPassService, - private readonly UserMasterPassService $userMasterPassService, - private readonly UserProfileService $userProfileService + Application $application, + TrackService $trackService, + RequestInterface $request, + private readonly AuthProviderService $authProviderService, + private readonly LanguageInterface $language, + private readonly UserService $userService, + private readonly LoginUserService $loginUserService, + private readonly LoginMasterPassService $loginMasterPassService, + private readonly UserProfileService $userProfileService, + private readonly LoginAuthHandlerService $loginAuthHandlerService ) { - parent::__construct($application); + parent::__construct($application, $trackService, $request); - $this->configData = $this->config->getConfigData(); - $this->userLoginData = new UserLoginDto(); - $this->trackRequest = $this->trackService->buildTrackRequest(__CLASS__); - $this->authProvider->initialize(); + $this->userLoginDto = new UserLoginDto(); + $this->authProviderService->initialize(); } /** - * Ejecutar las acciones de login + * @inheritDoc * * @return LoginResponseDto * @throws AuthException - * @throws SPException - * @throws EnvironmentIsBrokenException - * @throws ConstraintException - * @throws QueryException - * @throws Exception - * @uses Login::authBrowser() - * @uses Login::authDatabase() - * @uses Login::authLdap() - * */ - public function doLogin(): LoginResponseDto - { - $user = $this->request->analyzeString('user'); - $pass = $this->request->analyzeEncrypted('pass'); - - if (empty($user) || empty($pass)) { - $this->addTracking(); - - throw new AuthException( - __u('Wrong login'), - SPException::INFO, - __FUNCTION__, - self::STATUS_INVALID_LOGIN - ); - } - - $this->userLoginData->setLoginUser($user); - $this->userLoginData->setLoginPass($pass); - - if ($this->trackService->checkTracking($this->trackRequest)) { - $this->addTracking(); - - throw new AuthException( - __u('Attempts exceeded'), - SPException::INFO, - null, - self::STATUS_MAX_ATTEMPTS_EXCEEDED - ); - } - - $result = $this->authProvider->doAuth($this->userLoginData); - - if ($result !== false) { - // Ejecutar la acción asociada al tipo de autentificación - foreach ($result as $authResult) { - if (method_exists($this, $authResult->getAuthName())) { - $granted = $this->{$authResult->getAuthName()}($authResult->getData()); - - if ($granted) { - break; - } - } - } - } else { - $this->addTracking(); - - throw new AuthException( - __u('Wrong login'), - SPException::INFO, - __FUNCTION__, - self::STATUS_INVALID_LOGIN - ); - } - - if (($loginResponse = $this->checkUser())->getStatus() !== self::STATUS_NONE) { - return $loginResponse; - } - - $this->loadMasterPass(); - $this->setUserSession(); - $this->loadUserPreferences(); - $this->cleanUserData(); - - $uri = new Uri('index.php'); - - if (!empty($this->from)) { - $uri->addParam('r', $this->from); - } else { - $uri->addParam('r', 'index'); - } - - return new LoginResponseDto(self::STATUS_PASS, $uri->getUri()); - } - - /** - * Añadir un seguimiento - * - * @throws AuthException - */ - private function addTracking(): void + public function doLogin(?string $from = null): LoginResponseDto { try { - $this->trackService->add($this->trackRequest); - } catch (Exception $e) { - throw new AuthException( - __u('Internal error'), - SPException::ERROR, - null, - Service::STATUS_INTERNAL_ERROR, - $e - ); - } - } - - /** - * Comprobar estado del usuario - * - * @return LoginResponseDto - * @throws EnvironmentIsBrokenException - * @throws ConstraintException - * @throws QueryException - * @throws AuthException - */ - private function checkUser(): LoginResponseDto - { - $userLoginResponse = $this->userLoginData->getUserDataDto(); - - if ($userLoginResponse !== null) { - // Comprobar si el usuario está deshabilitado - if ($userLoginResponse->getIsDisabled()) { - $this->eventDispatcher->notify( - 'login.checkUser.disabled', - new Event( - $this, - EventMessage::factory() - ->addDescription(__u('User disabled')) - ->addDetail(__u('User'), $userLoginResponse->getLogin()) - ) - ); + $user = $this->request->analyzeString('user'); + $pass = $this->request->analyzeEncrypted('pass'); + if (empty($user) || empty($pass)) { $this->addTracking(); - throw new AuthException( - __u('User disabled'), - SPException::INFO, - null, - self::STATUS_USER_DISABLED + throw AuthException::info(__u('Wrong login'), __FUNCTION__, LoginStatus::INVALID_LOGIN->value); + } + + $this->userLoginDto->setLoginUser($user); + $this->userLoginDto->setLoginPass($pass); + + $this->checkTracking(); + + $userDataDto = $this->authProviderService->doAuth($this->userLoginDto, [$this, 'handleAuthResponse']); + + if ($userDataDto === null) { + throw ServiceException::error( + __u('Internal error'), + __u('Authoritative provider didn\'t return the user\'s data') ); } - // Check whether a user's password change has been requested - if ($userLoginResponse->getIsChangePass()) { - $this->eventDispatcher->notify( - 'login.checkUser.changePass', - new Event($this, EventMessage::factory()->addDetail(__u('User'), $userLoginResponse->getLogin())) - ); + $checkUser = $this->loginUserService->checkUser($userDataDto); - $hash = PasswordUtil::generateRandomBytes(16); - - $this->userPassRecoverService->add($userLoginResponse->getId(), $hash); - - $uri = new Uri('index.php'); - $uri->addParam('r', 'userPassReset/reset/' . $hash); - - return new LoginResponseDto(self::STATUS_PASS_RESET, $uri->getUri()); + if ($checkUser->getStatus() !== LoginStatus::PASS) { + return $checkUser; } - } - return new LoginResponseDto(self::STATUS_NONE); - } + $this->loginMasterPassService->loadMasterPass($this->userLoginDto, $userDataDto); + $this->setUserSession($userDataDto); + $this->loadUserPreferences(); - /** - * Cargar la clave maestra o solicitarla - * - * @throws AuthException - * @throws CryptException - * @throws NoSuchItemException - * @throws SPException - */ - private function loadMasterPass(): void - { - $masterPass = $this->request->analyzeEncrypted('mpass'); - $oldPass = $this->request->analyzeEncrypted('oldpass'); - - try { - if ($masterPass) { - $this->checkMasterPass($masterPass); - } elseif ($oldPass) { - $this->loadMasterPassUsingOld($oldPass); - } else { - switch ($this->userMasterPassService->load($this->userLoginData)->getUserMasterPassStatus()) { - case UserMasterPassStatus::CheckOld: - throw new AuthException( - __u('Your previous password is needed'), - SPException::INFO, - null, - self::STATUS_NEED_OLD_PASS - ); - case UserMasterPassStatus::NotSet: - case UserMasterPassStatus::Changed: - case UserMasterPassStatus::Invalid: - $this->addTracking(); - - throw new AuthException( - __u('The Master Password either is not saved or is wrong'), - SPException::INFO, - null, - self::STATUS_INVALID_MASTER_PASS - ); - case UserMasterPassStatus::Ok: - $this->eventDispatcher->notify( - 'login.masterPass', - new Event($this, EventMessage::factory()->addDescription(__u('Master password loaded'))) - ); - break; - } - } + return new LoginResponseDto(LoginStatus::OK, $this->getUriForRoute($from ?? 'index')); } catch (ServiceException $e) { - $this->eventDispatcher->notify('exception', new Event($e)); - - throw new AuthException( - __u('Internal error'), - SPException::ERROR, - $e->getMessage(), - Service::STATUS_INTERNAL_ERROR, - $e - ); + throw AuthException::from($e); } } /** - * @param string $masterPass - * @return void - * @throws AuthException - * @throws NoSuchItemException * @throws ServiceException - * @throws CryptException */ - private function checkMasterPass(string $masterPass): void + private function setUserSession(UserDataDto $userDataDto): void { - if ($this->temporaryMasterPassService->checkTempMasterPass($masterPass)) { - $this->eventDispatcher->notify( - 'login.masterPass.temporary', - new Event($this, EventMessage::factory()->addDescription(__u('Using temporary password'))) - ); - - $masterPass = $this->temporaryMasterPassService->getUsingKey($masterPass); - } - - if ($this->userMasterPassService->updateOnLogin($masterPass, $this->userLoginData) - ->getUserMasterPassStatus() !== UserMasterPassStatus::Ok - ) { - $this->eventDispatcher->notify( - 'login.masterPass', - new Event($this, EventMessage::factory()->addDescription(__u('Wrong master password'))) - ); - - $this->addTracking(); - - throw new AuthException( - __u('Wrong master password'), - SPException::INFO, - null, - self::STATUS_INVALID_MASTER_PASS - ); - } - - $this->eventDispatcher->notify( - 'login.masterPass', - new Event($this, EventMessage::factory()->addDescription(__u('Master password updated'))) - ); - } - - /** - * @param string $oldPass - * @return void - * @throws AuthException - * @throws SPException - */ - private function loadMasterPassUsingOld(string $oldPass): void - { - if ($this->userMasterPassService->updateFromOldPass($oldPass, $this->userLoginData) - ->getUserMasterPassStatus() !== UserMasterPassStatus::Ok - ) { - $this->eventDispatcher->notify( - 'login.masterPass', - new Event($this, EventMessage::factory()->addDescription(__u('Wrong master password'))) - ); - - $this->addTracking(); - - throw new AuthException( - __u('Wrong master password'), - SPException::INFO, - null, - self::STATUS_INVALID_MASTER_PASS - ); - } - - $this->eventDispatcher->notify( - 'login.masterPass', - new Event($this, EventMessage::factory()->addDescription(__u('Master password updated'))) - ); - } - - /** - * Cargar la sesión del usuario - * - * @throws ConstraintException - * @throws QueryException - * @throws NoSuchItemException - */ - private function setUserSession(): void - { - $userLoginResponse = $this->userLoginData->getUserDataDto(); - - // Actualizar el último login del usuario - $this->userService->updateLastLoginById($userLoginResponse->getId()); + try { + $this->userService->updateLastLoginById($userDataDto->getId()); // if ($this->context->getTrasientKey(UserMasterPass::SESSION_MASTERPASS_UPDATED)) { // $this->context->setTrasientKey('user_master_pass_last_update', time()); // } - // Cargar las variables de ussuario en la sesión - $this->context->setUserData($userLoginResponse); - $this->context->setUserProfile( - $this->userProfileService - ->getById($userLoginResponse->getUserProfileId()) - ->hydrate(ProfileData::class) - ); - $this->context->setLocale($userLoginResponse->getPreferences()->getLang()); + $this->context->setUserData($userDataDto); + $this->context->setUserProfile( + $this->userProfileService + ->getById($userDataDto->getUserProfileId()) + ->hydrate(ProfileData::class) + ); + $this->context->setLocale($userDataDto->getPreferences()->getLang()); - $this->eventDispatcher->notify( - 'login.session.load', - new Event($this, EventMessage::factory()->addDetail(__u('User'), $userLoginResponse->getLogin())) - ); + $this->eventDispatcher->notify( + 'login.session.load', + new Event($this, EventMessage::factory()->addDetail(__u('User'), $userDataDto->getLogin())) + ); + } catch (ConstraintException|NoSuchItemException|QueryException $e) { + throw ServiceException::from($e); + } } - /** - * Cargar las preferencias del usuario y comprobar si usa 2FA - */ private function loadUserPreferences(): void { $this->language->setLanguage(true); @@ -451,265 +163,21 @@ final class Login extends Service implements LoginService } /** - * Limpiar datos de usuario - */ - private function cleanUserData(): void - { - $this->userLoginData->setUserDataDto(); - } - - /** - * @param string|null $from - */ - public function setFrom(?string $from): void - { - $this->from = $from; - } - - /** - * Autentificación LDAP + * @inheritDoc * - * @param LdapAuthData $authData - * - * @return bool - * @throws SPException * @throws AuthException + * @uses LoginAuthHandlerService::authBrowser() + * @uses LoginAuthHandlerService::authDatabase() + * @uses LoginAuthHandlerService::authLdap() */ - private function authLdap(LdapAuthData $authData): bool + public function handleAuthResponse(AuthResult $authResult): void { - if ($authData->getStatusCode() > LdapCodeEnum::SUCCESS->value) { - $eventMessage = EventMessage::factory() - ->addDetail(__u('Type'), __FUNCTION__) - ->addDetail(__u('LDAP Server'), $authData->getServer()) - ->addDetail(__u('User'), $this->userLoginData->getLoginUser()); + $authType = $authResult->getAuthType()->value; - if ($authData->getStatusCode() === LdapCodeEnum::INVALID_CREDENTIALS->value) { - $eventMessage->addDescription(__u('Wrong login')); + if (method_exists($this->loginAuthHandlerService, $authType)) { + $authData = $authResult->getAuthData(); - $this->addTracking(); - - $this->eventDispatcher->notify('login.auth.ldap', new Event($this, $eventMessage)); - - throw new AuthException( - __u('Wrong login'), - SPException::INFO, - __FUNCTION__, - self::STATUS_INVALID_LOGIN - ); - } - - if ($authData->getStatusCode() === LdapAuthService::ACCOUNT_EXPIRED) { - $eventMessage->addDescription(__u('Account expired')); - - $this->eventDispatcher->notify('login.auth.ldap', new Event($this, $eventMessage)); - - throw new AuthException( - __u('Account expired'), - SPException::INFO, - __FUNCTION__, - self::STATUS_USER_DISABLED - ); - } - - if ($authData->getStatusCode() === LdapAuthService::ACCOUNT_NO_GROUPS) { - $eventMessage->addDescription(__u('User has no associated groups')); - - $this->eventDispatcher->notify('login.auth.ldap', new Event($this, $eventMessage)); - - throw new AuthException( - __u('User has no associated groups'), - SPException::INFO, - __FUNCTION__, - self::STATUS_USER_DISABLED - ); - } - - if ($authData->getStatusCode() === LdapCodeEnum::NO_SUCH_OBJECT->value - || $authData->isAuthoritative() === false - ) { - $eventMessage->addDescription(__u('Non authoritative auth')); - - $this->eventDispatcher->notify('login.auth.ldap', new Event($this, $eventMessage)); - - return false; - } - - $eventMessage->addDescription(__u('Internal error')); - - $this->eventDispatcher->notify( - 'login.auth.ldap', - new Event($this, $eventMessage) - ); - - throw new AuthException( - __u('Internal error'), - SPException::INFO, - __FUNCTION__, - Service::STATUS_INTERNAL_ERROR - ); + $this->loginAuthHandlerService->{$authType}($authData, $this->userLoginDto); } - - $this->eventDispatcher->notify( - 'login.auth.ldap', - new Event( - $this, - EventMessage::factory() - ->addDetail(__u('Type'), __FUNCTION__) - ->addDetail(__u('LDAP Server'), $authData->getServer()) - ) - ); - - try { - $userLoginRequest = new UserLoginRequest(); - $userLoginRequest->setLogin($this->userLoginData->getLoginUser()); - - if ($this->configData->isLdapDatabaseEnabled()) { - $userLoginRequest->setPassword($this->userLoginData->getLoginPass()); - } else { - // Use a random password when database fallback is disabled - $userLoginRequest->setPassword(PasswordUtil::randomPassword()); - } - - $userLoginRequest->setEmail($authData->getEmail()); - $userLoginRequest->setName($authData->getName()); - $userLoginRequest->setIsLdap(true); - - // Verificamos si el usuario existe en la BBDD - if ($this->userService->checkExistsByLogin($this->userLoginData->getLoginUser())) { - // Actualizamos el usuario de LDAP en MySQL - $this->userService->updateOnLogin($userLoginRequest); - } else { - // Creamos el usuario de LDAP en MySQL - $this->userService->createOnLogin($userLoginRequest); - } - } catch (Exception $e) { - throw new AuthException( - __u('Internal error'), - SPException::ERROR, - __FUNCTION__, - Service::STATUS_INTERNAL_ERROR, - $e - ); - } - - return true; - } - - /** - * Autentificación en BD - * - * @param DatabaseAuthData $authData - * - * @return bool - * @throws SPException - * @throws AuthException - */ - private function authDatabase(DatabaseAuthData $authData): bool - { - $eventMessage = EventMessage::factory() - ->addDetail(__u('Type'), __FUNCTION__) - ->addDetail(__u('User'), $this->userLoginData->getLoginUser()); - - // Autentificamos con la BBDD - if ($authData->getAuthenticated() === false) { - if ($authData->isAuthoritative() === false) { - $eventMessage->addDescription(__u('Non authoritative auth')); - - $this->eventDispatcher->notify('login.auth.database', new Event($this, $eventMessage)); - - return false; - } - - $this->addTracking(); - - $eventMessage->addDescription(__u('Wrong login')); - - $this->eventDispatcher->notify('login.auth.database', new Event($this, $eventMessage)); - - throw new AuthException( - __u('Wrong login'), - SPException::INFO, - __FUNCTION__, - self::STATUS_INVALID_LOGIN - ); - } - - $this->eventDispatcher->notify( - 'login.auth.database', - new Event($this, $eventMessage) - ); - - return true; - } - - /** - * Comprobar si el cliente ha enviado las variables de autentificación - * - * @param BrowserAuthData $authData - * - * @return bool - * @throws AuthException - */ - private function authBrowser(BrowserAuthData $authData): bool - { - $authType = $this->request->getServer('AUTH_TYPE') ?: __('N/A'); - - $eventMessage = EventMessage::factory() - ->addDetail(__u('Type'), __FUNCTION__) - ->addDetail(__u('User'), $this->userLoginData->getLoginUser()) - ->addDetail( - __u('Authentication'), - sprintf('%s (%s)', $authType, $authData->getName()) - ); - - // Comprobar si concide el login con la autentificación del servidor web - if ($authData->getAuthenticated() === false) { - if ($authData->isAuthoritative() === false) { - $eventMessage->addDescription(__u('Non authoritative auth')); - - $this->eventDispatcher->notify('login.auth.browser', new Event($this, $eventMessage)); - - return false; - } - - $this->addTracking(); - - $eventMessage->addDescription(__u('Wrong login')); - - $this->eventDispatcher->notify('login.auth.browser', new Event($this, $eventMessage)); - - throw new AuthException( - __u('Wrong login'), - SPException::INFO, - __FUNCTION__, - self::STATUS_INVALID_LOGIN - ); - } - - if ($this->configData->isAuthBasicAutoLoginEnabled()) { - try { - $userLoginRequest = new UserLoginRequest(); - $userLoginRequest->setLogin($this->userLoginData->getLoginUser()); - $userLoginRequest->setPassword($this->userLoginData->getLoginPass()); - - // Verificamos si el usuario existe en la BBDD - if (!$this->userService->checkExistsByLogin($this->userLoginData->getLoginUser())) { - // Creamos el usuario de SSO en la BBDD - $this->userService->createOnLogin($userLoginRequest); - } - - $this->eventDispatcher->notify('login.auth.browser', new Event($this, $eventMessage)); - } catch (Exception $e) { - throw new AuthException( - __u('Internal error'), - SPException::ERROR, - __FUNCTION__, - Service::STATUS_INTERNAL_ERROR, - $e - ); - } - } - - return true; } } diff --git a/lib/SP/Domain/Auth/Services/LoginAuthHandler.php b/lib/SP/Domain/Auth/Services/LoginAuthHandler.php new file mode 100644 index 00000000..5dcbe6e0 --- /dev/null +++ b/lib/SP/Domain/Auth/Services/LoginAuthHandler.php @@ -0,0 +1,233 @@ +. + */ + +namespace SP\Domain\Auth\Services; + +use Exception; +use SP\Core\Application; +use SP\Core\Events\Event; +use SP\Core\Events\EventMessage; +use SP\Domain\Auth\Dtos\UserLoginDto; +use SP\Domain\Auth\Ports\LdapAuthService; +use SP\Domain\Auth\Ports\LoginAuthHandlerService; +use SP\Domain\Common\Services\Service; +use SP\Domain\Config\Ports\ConfigDataInterface; +use SP\Domain\Core\Exceptions\InvalidArgumentException; +use SP\Domain\Http\RequestInterface; +use SP\Domain\Security\Ports\TrackService; +use SP\Domain\User\Dtos\UserLoginRequest; +use SP\Domain\User\Ports\UserService; +use SP\Providers\Auth\AuthType; +use SP\Providers\Auth\Browser\BrowserAuthData; +use SP\Providers\Auth\Database\DatabaseAuthData; +use SP\Providers\Auth\Ldap\LdapAuthData; +use SP\Providers\Auth\Ldap\LdapCodeEnum; + +use function SP\__; +use function SP\__u; + +/** + * Class LoginAuthHandler + */ +final class LoginAuthHandler extends LoginBase implements LoginAuthHandlerService +{ + private ConfigDataInterface $configData; + + /** + * @throws InvalidArgumentException + */ + public function __construct( + Application $application, + TrackService $trackService, + RequestInterface $request, + private readonly UserService $userService + ) { + parent::__construct($application, $trackService, $request); + + $this->configData = $this->config->getConfigData(); + } + + /** + * @inheritDoc + */ + public function authDatabase(DatabaseAuthData $authData, UserLoginDto $userLoginDto): void + { + $eventMessage = EventMessage::factory() + ->addDetail(__u('Type'), AuthType::Database->value) + ->addDetail(__u('User'), $userLoginDto->getLoginUser()); + + if (!$authData->isOk()) { + if ($authData->isAuthoritative() === false) { + $eventMessage->addDescription(__u('Non authoritative auth')); + + $this->eventDispatcher->notify('login.auth.database', new Event($this, $eventMessage)); + + return; + } + + $this->addTracking(); + + $eventMessage->addDescription(__u('Wrong login')); + + $this->eventDispatcher->notify('login.auth.database', new Event($this, $eventMessage)); + + throw AuthException::info(__u('Wrong login'), __FUNCTION__, LoginStatus::INVALID_LOGIN->value); + } + + $this->eventDispatcher->notify('login.auth.database', new Event($this, $eventMessage)); + } + + /** + * @inheritDoc + */ + public function authBrowser(BrowserAuthData $authData, UserLoginDto $userLoginDto): void + { + $authType = $this->request->getServer('AUTH_TYPE') ?: __('N/A'); + + $eventMessage = EventMessage::factory() + ->addDetail(__u('Type'), AuthType::Browser->value) + ->addDetail(__u('User'), $userLoginDto->getLoginUser()) + ->addDetail( + __u('Authentication'), + sprintf('%s (%s)', $authType, $authData->getName()) + ); + + if (!$authData->isOk()) { + if ($authData->isAuthoritative() === false) { + $eventMessage->addDescription(__u('Non authoritative auth')); + + $this->eventDispatcher->notify('login.auth.browser', new Event($this, $eventMessage)); + + return; + } + + $this->addTracking(); + + $eventMessage->addDescription(__u('Wrong login')); + + $this->eventDispatcher->notify('login.auth.browser', new Event($this, $eventMessage)); + + throw AuthException::info(__u('Wrong login'), __FUNCTION__, LoginStatus::INVALID_LOGIN->value); + } + + if ($this->configData->isAuthBasicAutoLoginEnabled()) { + try { + if (!$this->userService->checkExistsByLogin($userLoginDto->getLoginUser())) { + $userLoginRequest = new UserLoginRequest( + $userLoginDto->getLoginUser(), + $userLoginDto->getLoginPass() + ); + + $this->userService->createOnLogin($userLoginRequest); + } + + $this->eventDispatcher->notify('login.auth.browser', new Event($this, $eventMessage)); + } catch (Exception $e) { + throw AuthException::error( + __u('Internal error'), + __FUNCTION__, + Service::STATUS_INTERNAL_ERROR, + $e + ); + } + } + } + + /** + * @inheritDoc + */ + public function authLdap(LdapAuthData $authData, UserLoginDto $userLoginDto): void + { + $eventMessage = EventMessage::factory() + ->addDetail(__u('Type'), AuthType::Ldap->value) + ->addDetail(__u('LDAP Server'), $authData->getServer()) + ->addDetail(__u('User'), $userLoginDto->getLoginUser()); + + if (!$authData->isOk()) { + if ($authData->isAuthoritative() === false) { + $eventMessage->addDescription(__u('Non authoritative auth')); + + $this->eventDispatcher->notify('login.auth.ldap', new Event($this, $eventMessage)); + + return; + } + + if ($authData->getStatusCode() === LdapCodeEnum::INVALID_CREDENTIALS->value) { + $eventMessage->addDescription(__u('Wrong login')); + + $this->addTracking(); + + $this->eventDispatcher->notify('login.auth.ldap', new Event($this, $eventMessage)); + + throw AuthException::info(__u('Wrong login'), __FUNCTION__, LoginStatus::INVALID_LOGIN->value); + } + + if ($authData->getStatusCode() === LdapAuthService::ACCOUNT_EXPIRED) { + $eventMessage->addDescription(__u('Account expired')); + + $this->eventDispatcher->notify('login.auth.ldap', new Event($this, $eventMessage)); + + throw AuthException::info(__u('Account expired'), __FUNCTION__, LoginStatus::USER_DISABLED->value); + } + + if ($authData->getStatusCode() === LdapAuthService::ACCOUNT_NO_GROUPS) { + $eventMessage->addDescription(__u('User has no associated groups')); + + $this->eventDispatcher->notify('login.auth.ldap', new Event($this, $eventMessage)); + + throw AuthException::info( + __u('User has no associated groups'), + __FUNCTION__, + LoginStatus::USER_DISABLED->value + ); + } + + $eventMessage->addDescription(__u('Internal error')); + + $this->eventDispatcher->notify('login.auth.ldap', new Event($this, $eventMessage)); + + throw AuthException::info(__u('Internal error'), __FUNCTION__, Service::STATUS_INTERNAL_ERROR); + } + + $this->eventDispatcher->notify('login.auth.ldap', new Event($this, $eventMessage)); + + try { + $userLoginRequest = new UserLoginRequest( + $userLoginDto->getLoginUser(), + $this->configData->isLdapDatabaseEnabled() ?: $userLoginDto->getLoginPass(), + $authData->getName(), + $authData->getEmail(), + true + ); + + if ($this->userService->checkExistsByLogin($userLoginDto->getLoginUser())) { + $this->userService->updateOnLogin($userLoginRequest); + } else { + $this->userService->createOnLogin($userLoginRequest); + } + } catch (Exception $e) { + throw AuthException::error(__u('Internal error'), __FUNCTION__, Service::STATUS_INTERNAL_ERROR, $e); + } + } +} diff --git a/lib/SP/Domain/Auth/Services/LoginBase.php b/lib/SP/Domain/Auth/Services/LoginBase.php new file mode 100644 index 00000000..2dcd1f51 --- /dev/null +++ b/lib/SP/Domain/Auth/Services/LoginBase.php @@ -0,0 +1,89 @@ +. + */ + +namespace SP\Domain\Auth\Services; + +use Exception; +use SP\Core\Application; +use SP\Domain\Common\Services\Service; +use SP\Domain\Core\Exceptions\InvalidArgumentException; +use SP\Domain\Http\RequestInterface; +use SP\Domain\Security\Dtos\TrackRequest; +use SP\Domain\Security\Ports\TrackService; +use SP\Http\Uri; + +use function SP\__u; + +/** + * Class LoginBase + */ +abstract class LoginBase extends Service +{ + private TrackRequest $trackRequest; + + /** + * @throws InvalidArgumentException + */ + public function __construct( + Application $application, + private readonly TrackService $trackService, + protected readonly RequestInterface $request + ) { + parent::__construct($application); + + $this->trackRequest = $this->trackService->buildTrackRequest(static::class); + } + + /** + * @throws AuthException + * @throws Exception + */ + final protected function checkTracking(): void + { + if ($this->trackService->checkTracking($this->trackRequest)) { + $this->addTracking(); + + throw AuthException::error(__u('Attempts exceeded'), null, LoginStatus::MAX_ATTEMPTS_EXCEEDED->value); + } + } + + /** + * Añadir un seguimiento + * + * @throws AuthException + */ + final protected function addTracking(): void + { + try { + $this->trackService->add($this->trackRequest); + } catch (Exception $e) { + throw AuthException::error(__u('Internal error'), null, Service::STATUS_INTERNAL_ERROR, $e); + } + } + + protected function getUriForRoute(string $route): string + { + return (new Uri('index.php'))->addParam('r', $route)->getUri(); + } +} diff --git a/lib/SP/Domain/Auth/Services/LoginMasterPass.php b/lib/SP/Domain/Auth/Services/LoginMasterPass.php new file mode 100644 index 00000000..24a10275 --- /dev/null +++ b/lib/SP/Domain/Auth/Services/LoginMasterPass.php @@ -0,0 +1,171 @@ +. + */ + +namespace SP\Domain\Auth\Services; + +use SP\Core\Application; +use SP\Core\Events\Event; +use SP\Core\Events\EventMessage; +use SP\Domain\Auth\Dtos\UserLoginDto; +use SP\Domain\Auth\Ports\LoginMasterPassService; +use SP\Domain\Common\Services\Service; +use SP\Domain\Common\Services\ServiceException; +use SP\Domain\Core\Exceptions\CryptException; +use SP\Domain\Crypt\Ports\TemporaryMasterPassService; +use SP\Domain\Http\RequestInterface; +use SP\Domain\Security\Ports\TrackService; +use SP\Domain\User\Dtos\UserDataDto; +use SP\Domain\User\Ports\UserMasterPassService; +use SP\Domain\User\Services\UserMasterPassStatus; +use SP\Infrastructure\Common\Repositories\NoSuchItemException; + +use function SP\__u; + +/** + * Class LoginMasterPass + */ +final class LoginMasterPass extends LoginBase implements LoginMasterPassService +{ + public function __construct( + Application $application, + TrackService $trackService, + RequestInterface $request, + private readonly UserMasterPassService $userMasterPassService, + private readonly TemporaryMasterPassService $temporaryMasterPassService, + ) { + parent::__construct($application, $trackService, $request); + } + + /** + * @inheritDoc + */ + public function loadMasterPass(UserLoginDto $userLoginDto, UserDataDto $userDataDto): void + { + $masterPass = $this->request->analyzeEncrypted('mpass'); + $oldPass = $this->request->analyzeEncrypted('oldpass'); + + if ($masterPass) { + $this->checkMasterPass($masterPass, $userLoginDto); + } elseif ($oldPass) { + $this->loadMasterPassUsingOld($oldPass, $userLoginDto); + } else { + $this->loadCurrentMasterPass($userLoginDto, $userDataDto); + } + } + + /** + * @throws AuthException + * @throws ServiceException + */ + private function checkMasterPass(string $masterPass, UserLoginDto $userLoginDto): void + { + try { + if ($this->temporaryMasterPassService->checkTempMasterPass($masterPass)) { + $this->eventDispatcher->notify( + 'login.masterPass.temporary', + new Event($this, EventMessage::factory()->addDescription(__u('Using temporary password'))) + ); + + $masterPass = $this->temporaryMasterPassService->getUsingKey($masterPass); + } + + if ($this->userMasterPassService->updateOnLogin($masterPass, $userLoginDto) + ->getUserMasterPassStatus() !== UserMasterPassStatus::Ok + ) { + $this->eventDispatcher->notify( + 'login.masterPass', + new Event($this, EventMessage::factory()->addDescription(__u('Wrong master password'))) + ); + + $this->addTracking(); + + throw AuthException::info(__u('Wrong master password'), null, LoginStatus::INVALID_MASTER_PASS->value); + } + + $this->eventDispatcher->notify( + 'login.masterPass', + new Event($this, EventMessage::factory()->addDescription(__u('Master password updated'))) + ); + } catch (NoSuchItemException|CryptException $e) { + throw ServiceException::error('Internal error', __FUNCTION__, Service::STATUS_INTERNAL_ERROR, $e); + } + } + + /** + * @throws AuthException + * @throws ServiceException + */ + private function loadMasterPassUsingOld(string $oldPass, UserLoginDto $userLoginDto): void + { + if ($this->userMasterPassService->updateFromOldPass($oldPass, $userLoginDto) + ->getUserMasterPassStatus() !== UserMasterPassStatus::Ok + ) { + $this->eventDispatcher->notify( + 'login.masterPass', + new Event($this, EventMessage::factory()->addDescription(__u('Wrong master password'))) + ); + + $this->addTracking(); + + throw AuthException::info(__u('Wrong master password'), null, LoginStatus::INVALID_MASTER_PASS->value); + } + + $this->eventDispatcher->notify( + 'login.masterPass', + new Event($this, EventMessage::factory()->addDescription(__u('Master password updated'))) + ); + } + + /** + * @throws AuthException + * @throws ServiceException + */ + private function loadCurrentMasterPass(UserLoginDto $userLoginDto, UserDataDto $userDataDto): void + { + switch ($this->userMasterPassService->load($userLoginDto, $userDataDto)->getUserMasterPassStatus()) { + case UserMasterPassStatus::CheckOld: + throw AuthException::info( + __u('Your previous password is needed'), + null, + LoginStatus::OLD_PASS_REQUIRED->value + ); + case UserMasterPassStatus::NotSet: + case UserMasterPassStatus::Changed: + case UserMasterPassStatus::Invalid: + $this->addTracking(); + + throw AuthException::info( + __u('The Master Password either is not saved or is wrong'), + null, + LoginStatus::INVALID_MASTER_PASS->value + ); + case UserMasterPassStatus::Ok: + $this->eventDispatcher->notify( + 'login.masterPass', + new Event($this, EventMessage::factory()->addDescription(__u('Master password loaded'))) + ); + break; + } + } +} diff --git a/lib/SP/Providers/Auth/AuthProviderInterface.php b/lib/SP/Domain/Auth/Services/LoginStatus.php similarity index 64% rename from lib/SP/Providers/Auth/AuthProviderInterface.php rename to lib/SP/Domain/Auth/Services/LoginStatus.php index 360446d6..633f89d1 100644 --- a/lib/SP/Providers/Auth/AuthProviderInterface.php +++ b/lib/SP/Domain/Auth/Services/LoginStatus.php @@ -22,25 +22,19 @@ * along with sysPass. If not, see . */ -namespace SP\Providers\Auth; - -use SP\Domain\Auth\Dtos\UserLoginDto; +namespace SP\Domain\Auth\Services; /** - * Class Auth - * - * Esta clase es la encargada de realizar la autentificación de usuarios de sysPass. - * - * @package SP\Providers\Auth + * Class LoginStatus */ -interface AuthProviderInterface +enum LoginStatus: int { - /** - * Probar los métodos de autentificación - * - * @param UserLoginDto $userLoginData - * - * @return false|AuthResult[] - */ - public function doAuth(UserLoginDto $userLoginData): array|bool; + case OK = 0; + case INVALID_LOGIN = 1; + case INVALID_MASTER_PASS = 2; + case USER_DISABLED = 3; + case OLD_PASS_REQUIRED = 5; + case MAX_ATTEMPTS_EXCEEDED = 6; + case PASS_RESET_REQUIRED = 7; + case PASS = 100; } diff --git a/lib/SP/Domain/Auth/Services/LoginUser.php b/lib/SP/Domain/Auth/Services/LoginUser.php new file mode 100644 index 00000000..9a6d0c8b --- /dev/null +++ b/lib/SP/Domain/Auth/Services/LoginUser.php @@ -0,0 +1,107 @@ +. + */ + +namespace SP\Domain\Auth\Services; + +use Defuse\Crypto\Exception\EnvironmentIsBrokenException; +use SP\Core\Application; +use SP\Core\Events\Event; +use SP\Core\Events\EventMessage; +use SP\Domain\Auth\Dtos\LoginResponseDto; +use SP\Domain\Auth\Ports\LoginUserService; +use SP\Domain\Common\Services\Service; +use SP\Domain\Common\Services\ServiceException; +use SP\Domain\Core\Exceptions\ConstraintException; +use SP\Domain\Core\Exceptions\QueryException; +use SP\Domain\Http\RequestInterface; +use SP\Domain\Security\Ports\TrackService; +use SP\Domain\User\Dtos\UserDataDto; +use SP\Domain\User\Ports\UserPassRecoverService; +use SP\Util\PasswordUtil; + +use function SP\__u; + +/** + * Class LoginUser + */ +final class LoginUser extends LoginBase implements LoginUserService +{ + public function __construct( + Application $application, + TrackService $trackService, + RequestInterface $request, + private readonly UserPassRecoverService $userPassRecoverService + ) { + parent::__construct($application, $trackService, $request); + } + + /** + * Check the user status + * + * @param UserDataDto $userDataDto + * @return LoginResponseDto + * @throws AuthException + * @throws ServiceException + */ + public function checkUser(UserDataDto $userDataDto): LoginResponseDto + { + try { + if ($userDataDto->getIsDisabled()) { + $this->eventDispatcher->notify( + 'login.checkUser.disabled', + new Event( + $this, + EventMessage::factory() + ->addDescription(__u('User disabled')) + ->addDetail(__u('User'), $userDataDto->getLogin()) + ) + ); + + $this->addTracking(); + + throw AuthException::info(__u('User disabled'), null, LoginStatus::USER_DISABLED->value); + } + + if ($userDataDto->getIsChangePass()) { + $this->eventDispatcher->notify( + 'login.checkUser.changePass', + new Event($this, EventMessage::factory()->addDetail(__u('User'), $userDataDto->getLogin())) + ); + + $hash = PasswordUtil::generateRandomBytes(16); + + $this->userPassRecoverService->add($userDataDto->getId(), $hash); + + return new LoginResponseDto( + LoginStatus::PASS_RESET_REQUIRED, + $this->getUriForRoute('userPassReset/reset/' . $hash) + ); + } + + return new LoginResponseDto(LoginStatus::PASS); + } catch (EnvironmentIsBrokenException|ConstraintException|QueryException $e) { + throw ServiceException::error('Internal error', __FUNCTION__, Service::STATUS_INTERNAL_ERROR, $e); + } + } +} diff --git a/lib/SP/Domain/Core/Context/SessionContextInterface.php b/lib/SP/Domain/Core/Context/SessionContextInterface.php index 53bcdd74..68b2ac37 100644 --- a/lib/SP/Domain/Core/Context/SessionContextInterface.php +++ b/lib/SP/Domain/Core/Context/SessionContextInterface.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2024, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -52,7 +52,7 @@ interface SessionContextInterface extends ContextInterface public function getSearchFilters(): ?AccountSearchFilterDto; /** - * @param \SP\Domain\Account\Dtos\AccountSearchFilterDto $searchFilters + * @param AccountSearchFilterDto $searchFilters */ public function setSearchFilters(AccountSearchFilterDto $searchFilters): void; diff --git a/lib/SP/Domain/User/Ports/UserMasterPassService.php b/lib/SP/Domain/User/Ports/UserMasterPassService.php index 0500ab3c..3dfcc0fd 100644 --- a/lib/SP/Domain/User/Ports/UserMasterPassService.php +++ b/lib/SP/Domain/User/Ports/UserMasterPassService.php @@ -26,6 +26,7 @@ namespace SP\Domain\User\Ports; use SP\Domain\Auth\Dtos\UserLoginDto; use SP\Domain\Common\Services\ServiceException; +use SP\Domain\User\Dtos\UserDataDto; use SP\Domain\User\Dtos\UserMasterPassDto; /** @@ -36,29 +37,46 @@ use SP\Domain\User\Dtos\UserMasterPassDto; interface UserMasterPassService { /** - * Actualizar la clave maestra con la clave anterior del usuario + * Update the current user's master password with the previous user's login password * * @throws ServiceException */ - public function updateFromOldPass(string $oldUserPass, UserLoginDto $userLoginDto): UserMasterPassDto; + public function updateFromOldPass( + string $oldUserPass, + UserLoginDto $userLoginDto, + UserDataDto $userDataDto + ): UserMasterPassDto; /** - * Comprueba la clave maestra del usuario. + * Load the user's master password * * @throws ServiceException */ - public function load(UserLoginDto $userLoginDto, ?string $userPass = null): UserMasterPassDto; + public function load( + UserLoginDto $userLoginDto, + UserDataDto $userDataDto, + ?string $userPass = null + ): UserMasterPassDto; /** - * Actualizar la clave maestra del usuario al realizar login + * Update the user's master pass on log in. + * It requires the user's login data to build a secure key to store the master password * + * @param string $userMasterPass + * @param UserLoginDto $userLoginDto + * @param int $userId + * @return UserMasterPassDto * @throws ServiceException */ - public function updateOnLogin(string $userMasterPass, UserLoginDto $userLoginDto): UserMasterPassDto; + public function updateOnLogin(string $userMasterPass, UserLoginDto $userLoginDto, int $userId): UserMasterPassDto; /** - * Actualizar la clave maestra del usuario en la BBDD. + * Update the user's master password in the database * + * @param string $masterPass + * @param string $userLogin + * @param string $userPass + * @return UserMasterPassDto * @throws ServiceException */ public function create(string $masterPass, string $userLogin, string $userPass): UserMasterPassDto; diff --git a/lib/SP/Domain/User/Services/UserMasterPass.php b/lib/SP/Domain/User/Services/UserMasterPass.php index 331e4677..cd8571e1 100644 --- a/lib/SP/Domain/User/Services/UserMasterPass.php +++ b/lib/SP/Domain/User/Services/UserMasterPass.php @@ -34,6 +34,7 @@ use SP\Domain\Common\Services\ServiceException; use SP\Domain\Config\Ports\ConfigService; use SP\Domain\Core\Crypt\CryptInterface; use SP\Domain\Core\Exceptions\CryptException; +use SP\Domain\User\Dtos\UserDataDto; use SP\Domain\User\Dtos\UserMasterPassDto; use SP\Domain\User\Ports\UserMasterPassService; use SP\Domain\User\Ports\UserRepository; @@ -60,31 +61,32 @@ final class UserMasterPass extends Service implements UserMasterPassService } /** - * Update the master pass by using the old user's password - * - * @throws ServiceException + * @inheritDoc */ - public function updateFromOldPass(string $oldUserPass, UserLoginDto $userLoginDto): UserMasterPassDto - { - $response = $this->load($userLoginDto, $oldUserPass); + public function updateFromOldPass( + string $oldUserPass, + UserLoginDto $userLoginDto, + UserDataDto $userDataDto + ): UserMasterPassDto { + $response = $this->load($userLoginDto, $userDataDto, $oldUserPass); if ($response->getUserMasterPassStatus() === UserMasterPassStatus::Ok) { - return $this->updateOnLogin($response->getClearMasterPass(), $userLoginDto); + return $this->updateOnLogin($response->getClearMasterPass(), $userLoginDto, $userDataDto->getId()); } return new UserMasterPassDto(UserMasterPassStatus::Invalid); } /** - * Load the user's master pass - * - * @throws ServiceException + * @inheritDoc */ - public function load(UserLoginDto $userLoginDto, ?string $userPass = null): UserMasterPassDto - { + public function load( + UserLoginDto $userLoginDto, + UserDataDto $userDataDto, + ?string $userPass = null + ): UserMasterPassDto { try { - if (($userDataDto = $userLoginDto->getUserDataDto()) === null - || empty($userDataDto->getMPass()) + if (empty($userDataDto->getMPass()) || empty($userDataDto->getMKey()) || empty($systemMasterPassHash = $this->configService->getByParam(self::PARAM_MASTER_PWD)) ) { @@ -139,15 +141,11 @@ final class UserMasterPass extends Service implements UserMasterPassService } /** - * Update the user's master pass on log in. - * It requires the user's login data to build a secure key to store the master password - * - * @throws ServiceException + * @inheritDoc */ - public function updateOnLogin(string $userMasterPass, UserLoginDto $userLoginDto): UserMasterPassDto + public function updateOnLogin(string $userMasterPass, UserLoginDto $userLoginDto, int $userId): UserMasterPassDto { try { - $userData = $userLoginDto->getUserDataDto(); $systemMasterPassHash = $this->configService->getByParam(self::PARAM_MASTER_PWD); if (null === $systemMasterPassHash) { @@ -164,7 +162,7 @@ final class UserMasterPass extends Service implements UserMasterPassService ); $this->userRepository->updateMasterPassById( - $userData->getId(), + $userId, $response->getCryptMasterPass(), $response->getCryptSecuredKey() ); @@ -183,9 +181,7 @@ final class UserMasterPass extends Service implements UserMasterPassService } /** - * Actualizar la clave maestra del usuario en la BBDD. - * - * @throws ServiceException + * @inheritDoc */ public function create(string $masterPass, string $userLogin, string $userPass): UserMasterPassDto { diff --git a/lib/SP/Mvc/Controller/WebControllerHelper.php b/lib/SP/Mvc/Controller/WebControllerHelper.php index c619c88f..5e82206b 100644 --- a/lib/SP/Mvc/Controller/WebControllerHelper.php +++ b/lib/SP/Mvc/Controller/WebControllerHelper.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2024, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -32,7 +32,7 @@ use SP\Domain\Core\UI\ThemeInterface; use SP\Domain\Http\RequestInterface; use SP\Modules\Web\Controllers\Helpers\LayoutHelper; use SP\Mvc\View\TemplateInterface; -use SP\Providers\Auth\Browser\BrowserAuthInterface; +use SP\Providers\Auth\Browser\BrowserAuthService; /** * Class WebControllerHelper @@ -44,15 +44,15 @@ final class WebControllerHelper private AclInterface $acl; private RequestInterface $request; private PhpExtensionChecker $extensionChecker; - private TemplateInterface $template; - private BrowserAuthInterface $browser; - private LayoutHelper $layoutHelper; + private TemplateInterface $template; + private BrowserAuthService $browser; + private LayoutHelper $layoutHelper; public function __construct( SimpleControllerHelper $simpleControllerHelper, - TemplateInterface $template, - BrowserAuthInterface $browser, - LayoutHelper $layoutHelper + TemplateInterface $template, + BrowserAuthService $browser, + LayoutHelper $layoutHelper ) { $this->theme = $simpleControllerHelper->getTheme(); $this->router = $simpleControllerHelper->getRouter(); @@ -94,7 +94,7 @@ final class WebControllerHelper return $this->template; } - public function getBrowser(): BrowserAuthInterface + public function getBrowser(): BrowserAuthService { return $this->browser; } diff --git a/lib/SP/Providers/Auth/AuthDataBase.php b/lib/SP/Providers/Auth/AuthDataBase.php index 539afeac..0e3ea6ef 100644 --- a/lib/SP/Providers/Auth/AuthDataBase.php +++ b/lib/SP/Providers/Auth/AuthDataBase.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2024, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -24,6 +24,8 @@ namespace SP\Providers\Auth; +use SP\Domain\User\Dtos\UserDataDto; + /** * Class AuthDataBase */ @@ -40,8 +42,10 @@ abstract class AuthDataBase /** * @param bool $authoritative Whether this authentication is required to access to the application */ - public function __construct(private readonly bool $authoritative = false) - { + public function __construct( + private readonly bool $authoritative = false, + private readonly ?UserDataDto $userDataDto = null + ) { } public function getName(): ?string @@ -64,11 +68,6 @@ abstract class AuthDataBase $this->email = $email; } - public function getAuthenticated(): ?int - { - return $this->authenticated; - } - public function getServer(): ?string { return $this->server; @@ -121,4 +120,9 @@ abstract class AuthDataBase { return $this->authenticated && $this->success && !$this->failed; } + + public function getUserDataDto(): ?UserDataDto + { + return $this->userDataDto; + } } diff --git a/lib/SP/Providers/Auth/AuthProvider.php b/lib/SP/Providers/Auth/AuthProvider.php index fadeb08c..c3e6c59f 100644 --- a/lib/SP/Providers/Auth/AuthProvider.php +++ b/lib/SP/Providers/Auth/AuthProvider.php @@ -27,7 +27,7 @@ namespace SP\Providers\Auth; use SP\Core\Application; use SP\Domain\Auth\Dtos\UserLoginDto; use SP\Domain\Auth\Services\AuthException; -use SP\Domain\Core\Exceptions\SPException; +use SP\Domain\User\Dtos\UserDataDto; use SP\Providers\Provider; use SplObjectStorage; @@ -36,16 +36,14 @@ use function SP\__u; defined('APP_ROOT') || die(); /** - * Class Auth + * Class AuthProvider * * Esta clase es la encargada de realizar la autentificación de usuarios de sysPass. - * - * @package SP\Providers\Auth */ -class AuthProvider extends Provider implements AuthProviderInterface +final class AuthProvider extends Provider implements AuthProviderService { /** - * @var SplObjectStorage + * @var SplObjectStorage */ protected readonly SplObjectStorage $auths; @@ -67,45 +65,43 @@ class AuthProvider extends Provider implements AuthProviderInterface /** * Register authentication methods * - * @param AuthInterface $auth - * @param AuthTypeEnum $authTypeEnum + * @param AuthService $auth + * @param AuthType $authTypeEnum * @throws AuthException */ - public function registerAuth(AuthInterface $auth, AuthTypeEnum $authTypeEnum): void + public function registerAuth(AuthService $auth, AuthType $authTypeEnum): void { if ($this->auths->contains($auth)) { - throw new AuthException( - __u('Authentication already initialized'), - SPException::ERROR, - $auth::class - ); + throw AuthException::error(__u('Authentication already initialized'), $auth::class); } - $this->auths->attach($auth, $authTypeEnum->value); + $this->auths->attach($auth, $authTypeEnum); } /** - * Probar los métodos de autentificación - * - * @param UserLoginDto $userLoginData - * - * @return false|AuthResult[] + * @inheritDoc */ - public function doAuth(UserLoginDto $userLoginData): array|bool + public function doAuth(UserLoginDto $userLoginData, callable $callback): ?UserDataDto { - $authsResult = []; - $this->auths->rewind(); while ($this->auths->valid()) { - $auth = $this->auths->current(); - $authName = $this->auths->getInfo(); + $authResult = new AuthResult( + $this->auths->getInfo(), + $this->auths->current()->authenticate($userLoginData) + ); - $authsResult[] = new AuthResult($authName, $auth->authenticate($userLoginData)); + $callback($authResult); + + $authData = $authResult->getAuthData(); + + if ($authData->isAuthoritative() && $authData->isOk()) { + return $authData->getUserDataDto(); + } $this->auths->next(); } - return count($authsResult) > 0 ? $authsResult : false; + return null; } } diff --git a/lib/SP/Providers/Auth/AuthProviderService.php b/lib/SP/Providers/Auth/AuthProviderService.php new file mode 100644 index 00000000..edd87f86 --- /dev/null +++ b/lib/SP/Providers/Auth/AuthProviderService.php @@ -0,0 +1,46 @@ +. + */ + +namespace SP\Providers\Auth; + +use SP\Domain\Auth\Dtos\UserLoginDto; +use SP\Domain\User\Dtos\UserDataDto; + +/** + * Interface AuthProviderService + */ +interface AuthProviderService +{ + /** + * Authenticate using the registered authentication providers. + * + * It iterates over the registered authentication providers and returns whenever an authoritative provider + * successfully authenticates the user. + * + * @param UserLoginDto $userLoginData + * @param callable(AuthResult):void $callback A callback function to call after the authentication. + * @return UserDataDto|null + */ + public function doAuth(UserLoginDto $userLoginData, callable $callback): ?UserDataDto; +} diff --git a/lib/SP/Providers/Auth/AuthResult.php b/lib/SP/Providers/Auth/AuthResult.php index 764e1360..116c41b6 100644 --- a/lib/SP/Providers/Auth/AuthResult.php +++ b/lib/SP/Providers/Auth/AuthResult.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2024, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -24,37 +24,29 @@ namespace SP\Providers\Auth; +use SP\Providers\Auth\Browser\BrowserAuthData; +use SP\Providers\Auth\Database\DatabaseAuthData; +use SP\Providers\Auth\Ldap\LdapAuthData; + /** - * Class AuthData - * - * @package SP\Providers\Auth + * Class AuthResult */ -final class AuthResult +final readonly class AuthResult { - /** - * AuthResult constructor. - * - * @param string $authName - * @param AuthDataBase $data - */ - public function __construct(private readonly string $authName, private readonly AuthDataBase $data) - { + public function __construct( + private AuthType $authTypeEnum, + private LdapAuthData|DatabaseAuthData|BrowserAuthData $authData + ) { } - /** - * @return string - */ - public function getAuthName(): string + public function getAuthType(): AuthType { - return $this->authName; + return $this->authTypeEnum; } - /** - * @return AuthDataBase - */ - public function getData(): AuthDataBase + public function getAuthData(): LdapAuthData|DatabaseAuthData|BrowserAuthData { - return $this->data; + return $this->authData; } } diff --git a/lib/SP/Providers/Auth/AuthInterface.php b/lib/SP/Providers/Auth/AuthService.php similarity index 87% rename from lib/SP/Providers/Auth/AuthInterface.php rename to lib/SP/Providers/Auth/AuthService.php index aeadac9f..c009b86d 100644 --- a/lib/SP/Providers/Auth/AuthInterface.php +++ b/lib/SP/Providers/Auth/AuthService.php @@ -30,17 +30,16 @@ use SP\Domain\Auth\Dtos\UserLoginDto; * Interface AuthInterface * * @template T - * @package Auth */ -interface AuthInterface +interface AuthService { /** * Authenticate using user's data * - * @param UserLoginDto $userLoginData + * @param UserLoginDto $userLoginDto * @return T */ - public function authenticate(UserLoginDto $userLoginData): AuthDataBase; + public function authenticate(UserLoginDto $userLoginDto): AuthDataBase; /** * Indica si es requerida para acceder a la aplicación diff --git a/lib/SP/Providers/Auth/AuthTypeEnum.php b/lib/SP/Providers/Auth/AuthType.php similarity index 81% rename from lib/SP/Providers/Auth/AuthTypeEnum.php rename to lib/SP/Providers/Auth/AuthType.php index 73f47841..1067a6dd 100644 --- a/lib/SP/Providers/Auth/AuthTypeEnum.php +++ b/lib/SP/Providers/Auth/AuthType.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2024, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -25,12 +25,11 @@ namespace SP\Providers\Auth; /** - * Class AuthTypeEnum + * Class AuthType */ -enum AuthTypeEnum: string +enum AuthType: string { case Ldap = 'authLdap'; - case Browser = 'authDatabase'; - case Database = 'authBrowser'; - + case Browser = 'authBrowser'; + case Database = 'authDatabase'; } diff --git a/lib/SP/Providers/Auth/Browser/BrowserAuth.php b/lib/SP/Providers/Auth/Browser/BrowserAuth.php index 7bf3ae82..67a9d316 100644 --- a/lib/SP/Providers/Auth/Browser/BrowserAuth.php +++ b/lib/SP/Providers/Auth/Browser/BrowserAuth.php @@ -27,16 +27,16 @@ namespace SP\Providers\Auth\Browser; use SP\Domain\Auth\Dtos\UserLoginDto; use SP\Domain\Config\Ports\ConfigDataInterface; use SP\Domain\Http\RequestInterface; -use SP\Providers\Auth\AuthInterface; +use SP\Providers\Auth\AuthService; /** * Class Browser * * Autentificación basada en credenciales del navegador * - * @implements AuthInterface + * @implements AuthService */ -final class BrowserAuth implements BrowserAuthInterface +final class BrowserAuth implements BrowserAuthService { private ConfigDataInterface $configData; private RequestInterface $request; @@ -50,16 +50,16 @@ final class BrowserAuth implements BrowserAuthInterface /** * Authenticate using user's data * - * @param UserLoginDto $userLoginData + * @param UserLoginDto $userLoginDto * @return BrowserAuthData */ - public function authenticate(UserLoginDto $userLoginData): BrowserAuthData + public function authenticate(UserLoginDto $userLoginDto): BrowserAuthData { $browserAuthData = new BrowserAuthData($this->isAuthGranted()); - if (!empty($userLoginData->getLoginUser()) - && !empty($userLoginData->getLoginPass()) - && $this->checkServerAuthUser($userLoginData->getLoginUser()) + if (!empty($userLoginDto->getLoginUser()) + && !empty($userLoginDto->getLoginPass()) + && $this->checkServerAuthUser($userLoginDto->getLoginUser()) ) { return $browserAuthData->success(); } @@ -69,8 +69,8 @@ final class BrowserAuth implements BrowserAuthInterface $authPass = $this->getAuthPass(); if ($authUser !== null && $authPass !== null) { - $userLoginData->setLoginUser($authUser); - $userLoginData->setLoginPass($authPass); + $userLoginDto->setLoginUser($authUser); + $userLoginDto->setLoginPass($authPass); $browserAuthData->setName($authUser); @@ -80,7 +80,7 @@ final class BrowserAuth implements BrowserAuthInterface return $browserAuthData->fail(); } - return $this->checkServerAuthUser($userLoginData->getLoginUser()) + return $this->checkServerAuthUser($userLoginDto->getLoginUser()) ? $browserAuthData->success() : $browserAuthData->fail(); } diff --git a/lib/SP/Providers/Auth/Browser/BrowserAuthInterface.php b/lib/SP/Providers/Auth/Browser/BrowserAuthService.php similarity index 86% rename from lib/SP/Providers/Auth/Browser/BrowserAuthInterface.php rename to lib/SP/Providers/Auth/Browser/BrowserAuthService.php index 19179197..8f2292fc 100644 --- a/lib/SP/Providers/Auth/Browser/BrowserAuthInterface.php +++ b/lib/SP/Providers/Auth/Browser/BrowserAuthService.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2024, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -24,16 +24,16 @@ namespace SP\Providers\Auth\Browser; -use SP\Providers\Auth\AuthInterface; +use SP\Providers\Auth\AuthService; /** * Class Browser * * Autentificación basada en credenciales del navegador * - * @extends AuthInterface + * @extends AuthService */ -interface BrowserAuthInterface extends AuthInterface +interface BrowserAuthService extends AuthService { /** * Comprobar si el usuario es autentificado por el servidor web diff --git a/lib/SP/Providers/Auth/Database/DatabaseAuth.php b/lib/SP/Providers/Auth/Database/DatabaseAuth.php index f7336485..d3ba3d61 100644 --- a/lib/SP/Providers/Auth/Database/DatabaseAuth.php +++ b/lib/SP/Providers/Auth/Database/DatabaseAuth.php @@ -47,14 +47,50 @@ final readonly class DatabaseAuth implements DatabaseAuthService /** * Authenticate using user's data * - * @param UserLoginDto $userLoginData + * @param UserLoginDto $userLoginDto * @return DatabaseAuthData */ - public function authenticate(UserLoginDto $userLoginData): DatabaseAuthData + public function authenticate(UserLoginDto $userLoginDto): DatabaseAuthData { - $authData = new DatabaseAuthData($this->isAuthGranted()); + $authUser = $this->authUser($userLoginDto); - return $this->authUser($userLoginData) ? $authData->success() : $authData->fail(); + $authData = new DatabaseAuthData($this->isAuthGranted(), $authUser ?: null); + + return $authUser ? $authData->success() : $authData->fail(); + } + + private function authUser(UserLoginDto $userLoginDto): UserDataDto|false + { + try { + $userDataDto = new UserDataDto($this->userService->getByLogin($userLoginDto->getLoginUser())); + + if ($userDataDto->getIsMigrate() && $this->checkMigrateUser($userDataDto, $userLoginDto)) { + $this->userPassService->migrateUserPassById($userDataDto->getId(), $userLoginDto->getLoginPass()); + + return $userDataDto; + } + + if (Hash::checkHashKey($userLoginDto->getLoginPass(), $userDataDto->getPass())) { + return $userDataDto; + } + } catch (Exception $e) { + processException($e); + } + + return false; + } + + private function checkMigrateUser(UserDataDto $userDataDto, UserLoginDto $userLoginDto): bool + { + $passHashSha = sha1($userDataDto->getHashSalt() . $userLoginDto->getLoginPass()); + + return ($userDataDto->getPass() === $passHashSha + || $userDataDto->getPass() === md5($userLoginDto->getLoginPass()) + || hash_equals( + $userDataDto->getPass(), + crypt($userLoginDto->getLoginPass(), $userDataDto->getHashSalt()) + ) + || Hash::checkHashKey($userLoginDto->getLoginPass(), $userDataDto->getPass())); } /** @@ -66,41 +102,4 @@ final readonly class DatabaseAuth implements DatabaseAuthService { return true; } - - private function authUser(UserLoginDto $userLoginData): bool - { - try { - $userLoginResponse = new UserDataDto($this->userService->getByLogin($userLoginData->getLoginUser())); - - $userLoginData->setUserDataDto($userLoginResponse); - - if ($userLoginResponse->getIsMigrate() && $this->checkMigrateUser($userLoginResponse, $userLoginData)) { - $this->userPassService->migrateUserPassById( - $userLoginResponse->getId(), - $userLoginData->getLoginPass() - ); - - return true; - } - - return Hash::checkHashKey($userLoginData->getLoginPass(), $userLoginResponse->getPass()); - } catch (Exception $e) { - processException($e); - } - - return false; - } - - private function checkMigrateUser(UserDataDto $userLoginResponse, UserLoginDto $userLoginData): bool - { - $passHashSha = sha1($userLoginResponse->getHashSalt() . $userLoginData->getLoginPass()); - - return ($userLoginResponse->getPass() === $passHashSha - || $userLoginResponse->getPass() === md5($userLoginData->getLoginPass()) - || hash_equals( - $userLoginResponse->getPass(), - crypt($userLoginData->getLoginPass(), $userLoginResponse->getHashSalt()) - ) - || Hash::checkHashKey($userLoginData->getLoginPass(), $userLoginResponse->getPass())); - } } diff --git a/lib/SP/Providers/Auth/Database/DatabaseAuthService.php b/lib/SP/Providers/Auth/Database/DatabaseAuthService.php index 7b9c5dc4..b2ca5b83 100644 --- a/lib/SP/Providers/Auth/Database/DatabaseAuthService.php +++ b/lib/SP/Providers/Auth/Database/DatabaseAuthService.php @@ -24,16 +24,16 @@ namespace SP\Providers\Auth\Database; -use SP\Providers\Auth\AuthInterface; +use SP\Providers\Auth\AuthService; /** * Class Database * * Autentificación basada en base de datos * - * @extends AuthInterface + * @extends AuthService */ -interface DatabaseAuthService extends AuthInterface +interface DatabaseAuthService extends AuthService { /** * Indica si es requerida para acceder a la aplicación diff --git a/lib/SP/Providers/Auth/Ldap/LdapAuth.php b/lib/SP/Providers/Auth/Ldap/LdapAuth.php index d244a741..05a37d98 100644 --- a/lib/SP/Providers/Auth/Ldap/LdapAuth.php +++ b/lib/SP/Providers/Auth/Ldap/LdapAuth.php @@ -41,7 +41,7 @@ use function SP\processException; * * @implements LdapService */ -final class LdapAuth implements LdapAuthService +final readonly class LdapAuth implements LdapAuthService { /** * LdapBase constructor. @@ -51,19 +51,19 @@ final class LdapAuth implements LdapAuthService * @param ConfigDataInterface $configData */ public function __construct( - private readonly LdapService $ldap, - private readonly EventDispatcherInterface $eventDispatcher, - private readonly ConfigDataInterface $configData + private LdapService $ldap, + private EventDispatcherInterface $eventDispatcher, + private ConfigDataInterface $configData ) { } /** * Authenticate using user's data * - * @param UserLoginDto $userLoginData + * @param UserLoginDto $userLoginDto * @return LdapAuthData */ - public function authenticate(UserLoginDto $userLoginData): LdapAuthData + public function authenticate(UserLoginDto $userLoginDto): LdapAuthData { $ldapAuthData = new LdapAuthData($this->isAuthGranted()); @@ -72,7 +72,7 @@ final class LdapAuth implements LdapAuthService $this->ldap->connect(); - $this->getAttributes($userLoginData->getLoginUser(), $ldapAuthData); + $this->getAttributes($userLoginDto->getLoginUser(), $ldapAuthData); // Comprobamos si la cuenta está bloqueada o expirada if ($ldapAuthData->getExpire() > 0) { @@ -85,7 +85,7 @@ final class LdapAuth implements LdapAuthService return $ldapAuthData->fail(); } - $this->ldap->connect($ldapAuthData->getDn(), $userLoginData->getLoginPass()); + $this->ldap->connect($ldapAuthData->getDn(), $userLoginDto->getLoginPass()); return $ldapAuthData->success(); } catch (LdapException $e) { diff --git a/tests/SPT/Domain/Auth/Services/LoginTest.php b/tests/SPT/Domain/Auth/Services/LoginTest.php new file mode 100644 index 00000000..22a85266 --- /dev/null +++ b/tests/SPT/Domain/Auth/Services/LoginTest.php @@ -0,0 +1,539 @@ +. + */ + +namespace SPT\Domain\Auth\Services; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\MockObject\Exception; +use PHPUnit\Framework\MockObject\MockObject; +use RuntimeException; +use SP\Core\Context\ContextException; +use SP\DataModel\ProfileData; +use SP\Domain\Auth\Dtos\LoginResponseDto; +use SP\Domain\Auth\Dtos\UserLoginDto; +use SP\Domain\Auth\Ports\LoginAuthHandlerService; +use SP\Domain\Auth\Ports\LoginMasterPassService; +use SP\Domain\Auth\Ports\LoginUserService; +use SP\Domain\Auth\Services\AuthException; +use SP\Domain\Auth\Services\Login; +use SP\Domain\Auth\Services\LoginStatus; +use SP\Domain\Common\Services\ServiceException; +use SP\Domain\Core\Context\ContextInterface; +use SP\Domain\Core\Context\SessionContextInterface; +use SP\Domain\Core\Exceptions\InvalidArgumentException; +use SP\Domain\Core\LanguageInterface; +use SP\Domain\Http\RequestInterface; +use SP\Domain\Security\Dtos\TrackRequest; +use SP\Domain\Security\Ports\TrackService; +use SP\Domain\User\Dtos\UserDataDto; +use SP\Domain\User\Ports\UserProfileService; +use SP\Domain\User\Ports\UserService; +use SP\Providers\Auth\AuthDataBase; +use SP\Providers\Auth\AuthProviderService; +use SP\Providers\Auth\AuthResult; +use SP\Providers\Auth\AuthType; +use SP\Providers\Auth\Browser\BrowserAuthData; +use SP\Providers\Auth\Database\DatabaseAuthData; +use SP\Providers\Auth\Ldap\LdapAuthData; +use SP\Providers\ProviderInterface; +use SPT\Generators\UserDataGenerator; +use SPT\Generators\UserProfileDataGenerator; +use SPT\UnitaryTestCase; + +/** + * Class LoginTest + * + * @property SessionContextInterface|MockObject $context + */ +#[Group('unitary')] +class LoginTest extends UnitaryTestCase +{ + + private TrackService|MockObject $trackService; + private RequestInterface|MockObject $request; + private MockObject|AuthProviderService|ProviderInterface $authProviderService; + private MockObject|LanguageInterface $language; + private UserService|MockObject $userService; + private LoginUserService|MockObject $loginUserService; + private MockObject|LoginMasterPassService $loginMasterPassService; + private MockObject|UserProfileService $userProfileService; + private MockObject|LoginAuthHandlerService $loginAuthHandlerService; + private Login $login; + + public static function authResultProviderAuthoritative(): array + { + $authResultDatabase = new AuthResult(AuthType::Database, (new DatabaseAuthData(true))->success()); + $authResultBrowser = new AuthResult(AuthType::Browser, (new BrowserAuthData(true))->success()); + $authResultLdap = new AuthResult(AuthType::Ldap, (new LdapAuthData(true))->success()); + + return array_map( + static fn(AuthResult $authResult) => [ + $authResult, + $authResult->getAuthData(), + $authResult->getAuthType()->value + ], + [ + $authResultDatabase, + $authResultBrowser, + $authResultLdap + ] + ); + } + + public static function authResultProviderNonAuthoritative(): array + { + $authResultDatabase = new AuthResult(AuthType::Database, (new DatabaseAuthData(false))->success()); + $authResultBrowser = new AuthResult(AuthType::Browser, (new BrowserAuthData(false))->success()); + $authResultLdap = new AuthResult(AuthType::Ldap, (new LdapAuthData(false))->success()); + + return array_map( + static fn(AuthResult $authResult) => [ + $authResult, + $authResult->getAuthData(), + $authResult->getAuthType()->value + ], + [ + $authResultDatabase, + $authResultBrowser, + $authResultLdap + ] + ); + } + + public static function loginInputProvider(): array + { + return [ + ['', 'a_pass'], + ['a_user', ''] + ]; + } + + public static function loginStatusDataProvider(): array + { + return [ + [LoginStatus::INVALID_LOGIN], + [LoginStatus::PASS_RESET_REQUIRED], + [LoginStatus::USER_DISABLED], + [LoginStatus::MAX_ATTEMPTS_EXCEEDED], + [LoginStatus::INVALID_MASTER_PASS], + [LoginStatus::OLD_PASS_REQUIRED], + [LoginStatus::OK], + ]; + } + + public static function fromDataProvider(): array + { + return [ + [null, 'index.php?r=index'], + ['a_test', 'index.php?r=a_test'], + ]; + } + + /** + * @throws AuthException + */ + #[DataProvider('authResultProviderAuthoritative')] + #[DataProvider('authResultProviderNonAuthoritative')] + public function testHandleAuthResponseWithTrue( + AuthResult $authResult, + AuthDataBase $authDataBase, + string $targetMethod + ) { + $this->loginAuthHandlerService + ->expects($this->once()) + ->method($targetMethod) + ->with($authDataBase); + + $this->login->handleAuthResponse($authResult); + } + + /** + * @throws AuthException + */ + #[DataProvider('authResultProviderAuthoritative')] + #[DataProvider('authResultProviderNonAuthoritative')] + public function testHandleAuthResponseWithFalse( + AuthResult $authResult, + AuthDataBase $authDataBase, + string $targetMethod + ) { + $this->loginAuthHandlerService + ->expects($this->once()) + ->method($targetMethod) + ->with($authDataBase); + + $this->login->handleAuthResponse($authResult); + } + + /** + * @throws AuthException + */ + #[DataProvider('fromDataProvider')] + public function testDoLogin(?string $from, string $redirect) + { + $userDataDto = new UserDataDto(UserDataGenerator::factory()->buildUserData()); + + $this->request + ->expects($this->once()) + ->method('analyzeString') + ->with('user') + ->willReturn('a_user'); + + $this->request + ->expects($this->once()) + ->method('analyzeEncrypted') + ->with('pass') + ->willReturn('a_password'); + + $this->trackService + ->expects($this->once()) + ->method('checkTracking') + ->willReturn(false); + + $this->authProviderService + ->expects($this->once()) + ->method('doAuth') + ->with( + self::callback(function (UserLoginDto $userLoginDto) { + return $userLoginDto->getLoginUser() === 'a_user' + && $userLoginDto->getLoginPass() === 'a_password'; + }), + self::callback(function (array $callable) { + return $callable[0] instanceof Login + && $callable[1] === 'handleAuthResponse'; + }) + ) + ->willReturn($userDataDto); + + $this->loginUserService + ->expects($this->once()) + ->method('checkUser') + ->with($userDataDto) + ->willReturn(new LoginResponseDto(LoginStatus::PASS)); + + $this->loginMasterPassService + ->expects($this->once()) + ->method('loadMasterPass') + ->with( + self::callback(function (UserLoginDto $userLoginDto) { + return $userLoginDto->getLoginUser() === 'a_user' + && $userLoginDto->getLoginPass() === 'a_password'; + }), + $userDataDto + ); + + $this->userService + ->expects($this->once()) + ->method('updateLastLoginById') + ->with($userDataDto->getId()); + + $this->context + ->expects($this->once()) + ->method('setUserData') + ->with($userDataDto); + + $userProfile = UserProfileDataGenerator::factory()->buildUserProfileData(); + + $this->userProfileService + ->expects($this->once()) + ->method('getById') + ->with($userDataDto->getUserProfileId()) + ->willReturn($userProfile); + + $this->context + ->expects($this->once()) + ->method('setUserProfile') + ->with($userProfile->hydrate(ProfileData::class)); + + $this->language + ->expects($this->once()) + ->method('setLanguage') + ->with(true); + + $this->context + ->expects($this->once()) + ->method('setAuthCompleted') + ->with(true); + + $out = $this->login->doLogin($from); + + $this->assertEquals(LoginStatus::OK, $out->getStatus()); + $this->assertEquals($redirect, $out->getRedirect()); + } + + /** + * @throws AuthException + */ + #[DataProvider('loginInputProvider')] + public function testDoLoginWithEmptyUserOrPass(string $user, string $pass) + { + $this->request + ->expects($this->once()) + ->method('analyzeString') + ->with('user') + ->willReturn($user); + + $this->request + ->expects($this->once()) + ->method('analyzeEncrypted') + ->with('pass') + ->willReturn($pass); + + $this->trackService + ->expects($this->once()) + ->method('add'); + + $this->expectException(AuthException::class); + $this->expectExceptionMessage('Wrong login'); + + $this->login->doLogin(); + } + + /** + * @throws AuthException + */ + public function testDoLoginWithNullUserData() + { + $userDataDto = new UserDataDto(UserDataGenerator::factory()->buildUserData()); + + $this->request + ->expects($this->once()) + ->method('analyzeString') + ->with('user') + ->willReturn('a_user'); + + $this->request + ->expects($this->once()) + ->method('analyzeEncrypted') + ->with('pass') + ->willReturn('a_password'); + + $this->trackService + ->expects($this->once()) + ->method('checkTracking') + ->willReturn(false); + + $this->authProviderService + ->expects($this->once()) + ->method('doAuth') + ->with( + self::callback(function (UserLoginDto $userLoginDto) { + return $userLoginDto->getLoginUser() === 'a_user' + && $userLoginDto->getLoginPass() === 'a_password'; + }), + self::callback(function (array $callable) { + return $callable[0] instanceof Login + && $callable[1] === 'handleAuthResponse'; + }) + ) + ->willReturn(null); + + $this->expectException(AuthException::class); + $this->expectExceptionMessage('Internal error'); + + $this->login->doLogin(); + } + + /** + * @throws AuthException + */ + #[DataProvider('loginStatusDataProvider')] + public function testDoLoginWithCheckUserFail(LoginStatus $loginStatus) + { + $userDataDto = new UserDataDto(UserDataGenerator::factory()->buildUserData()); + + $this->request + ->expects($this->once()) + ->method('analyzeString') + ->with('user') + ->willReturn('a_user'); + + $this->request + ->expects($this->once()) + ->method('analyzeEncrypted') + ->with('pass') + ->willReturn('a_password'); + + $this->trackService + ->expects($this->once()) + ->method('checkTracking') + ->willReturn(false); + + $this->authProviderService + ->expects($this->once()) + ->method('doAuth') + ->with( + self::callback(function (UserLoginDto $userLoginDto) { + return $userLoginDto->getLoginUser() === 'a_user' + && $userLoginDto->getLoginPass() === 'a_password'; + }), + self::callback(function (array $callable) { + return $callable[0] instanceof Login + && $callable[1] === 'handleAuthResponse'; + }) + ) + ->willReturn($userDataDto); + + $this->loginUserService + ->expects($this->once()) + ->method('checkUser') + ->with($userDataDto) + ->willReturn(new LoginResponseDto($loginStatus)); + + $out = $this->login->doLogin(); + + $this->assertEquals($loginStatus, $out->getStatus()); + } + + /** + * @throws AuthException + */ + public function testDoLoginWithServiceException() + { + $userDataDto = new UserDataDto(UserDataGenerator::factory()->buildUserData()); + + $this->request + ->expects($this->once()) + ->method('analyzeString') + ->with('user') + ->willReturn('a_user'); + + $this->request + ->expects($this->once()) + ->method('analyzeEncrypted') + ->with('pass') + ->willReturn('a_password'); + + $this->trackService + ->expects($this->once()) + ->method('checkTracking') + ->willReturn(false); + + $this->authProviderService + ->expects($this->once()) + ->method('doAuth') + ->willThrowException(ServiceException::error('test')); + + $this->expectException(AuthException::class); + $this->expectExceptionMessage('test'); + + $this->login->doLogin(); + } + + /** + * @throws AuthException + */ + public function testDoLoginWithException() + { + $userDataDto = new UserDataDto(UserDataGenerator::factory()->buildUserData()); + + $this->request + ->expects($this->once()) + ->method('analyzeString') + ->with('user') + ->willReturn('a_user'); + + $this->request + ->expects($this->once()) + ->method('analyzeEncrypted') + ->with('pass') + ->willReturn('a_password'); + + $this->trackService + ->expects($this->once()) + ->method('checkTracking') + ->willReturn(false); + + $this->authProviderService + ->expects($this->once()) + ->method('doAuth') + ->willThrowException(new RuntimeException('test')); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('test'); + + $this->login->doLogin(); + } + + /** + * @throws Exception + */ + protected function buildContext(): ContextInterface + { + return $this->createMock(SessionContextInterface::class); + } + + /** + * @throws Exception + * @throws ContextException + * @throws InvalidArgumentException + */ + protected function setUp(): void + { + parent::setUp(); + + $this->trackService = $this->createMock(TrackService::class); + $this->trackService + ->expects($this->once()) + ->method('buildTrackRequest') + ->with(Login::class) + ->willReturn( + new TrackRequest( + self::$faker->unixTime(), + self::$faker->colorName(), + self::$faker->ipv4(), + self::$faker->randomNumber(2) + ) + ); + + $this->request = $this->createMock(RequestInterface::class); + $this->authProviderService = $this->createMockForIntersectionOfInterfaces( + [AuthProviderService::class, ProviderInterface::class] + ); + $this->authProviderService + ->expects($this->once()) + ->method('initialize'); + + $this->language = $this->createMock(LanguageInterface::class); + $this->userService = $this->createMock(UserService::class); + $this->loginUserService = $this->createMock(LoginUserService::class); + $this->loginMasterPassService = $this->createMock(LoginMasterPassService::class); + $this->userProfileService = $this->createMock(UserProfileService::class); + $this->loginAuthHandlerService = $this->createMock(LoginAuthHandlerService::class); + + $this->login = new Login( + $this->application, + $this->trackService, + $this->request, + $this->authProviderService, + $this->language, + $this->userService, + $this->loginUserService, + $this->loginMasterPassService, + $this->userProfileService, + $this->loginAuthHandlerService + ); + } +} diff --git a/tests/SPT/Domain/Export/Services/XmlVerifyTest.php b/tests/SPT/Domain/Export/Services/XmlVerifyTest.php index ee4b2593..b053b5dc 100644 --- a/tests/SPT/Domain/Export/Services/XmlVerifyTest.php +++ b/tests/SPT/Domain/Export/Services/XmlVerifyTest.php @@ -25,6 +25,7 @@ namespace SPT\Domain\Export\Services; use DOMDocument; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\MockObject\MockObject; use SP\Core\Crypt\Hash; use SP\Domain\Common\Services\ServiceException; @@ -35,7 +36,6 @@ use SP\Domain\Core\Crypt\CryptInterface; use SP\Domain\Core\Exceptions\CryptException; use SP\Domain\Export\Services\XmlVerify; use SPT\UnitaryTestCase; -use PHPUnit\Framework\Attributes\Group; /** * Class XmlVerifyTest @@ -230,7 +230,7 @@ class XmlVerifyTest extends UnitaryTestCase $this->xmlVerify->verify(self::VALID_ENCRYPTED_FILE, 'test_encrypt'); } - protected function getConfig(): ConfigFileService + protected function buildConfig(): ConfigFileService { $configData = new ConfigData([ConfigDataInterface::PASSWORD_SALT => 'a_salt']); diff --git a/tests/SPT/Domain/Notification/Services/MailTest.php b/tests/SPT/Domain/Notification/Services/MailTest.php index 226eda03..298b2fa0 100644 --- a/tests/SPT/Domain/Notification/Services/MailTest.php +++ b/tests/SPT/Domain/Notification/Services/MailTest.php @@ -296,7 +296,7 @@ class MailTest extends UnitaryTestCase /** * @throws Exception */ - protected function getConfig(): ConfigFileService + protected function buildConfig(): ConfigFileService { $configData = ConfigDataGenerator::factory()->buildConfigData(); $configData->setMailServer(self::$faker->domainName()); diff --git a/tests/SPT/Domain/User/Services/UserMasterPassTest.php b/tests/SPT/Domain/User/Services/UserMasterPassTest.php index c25efc12..f97dec18 100644 --- a/tests/SPT/Domain/User/Services/UserMasterPassTest.php +++ b/tests/SPT/Domain/User/Services/UserMasterPassTest.php @@ -34,6 +34,7 @@ use SP\Domain\Config\Ports\ConfigService; use SP\Domain\Core\Crypt\CryptInterface; use SP\Domain\Core\Exceptions\CryptException; use SP\Domain\User\Dtos\UserDataDto; +use SP\Domain\User\Models\User; use SP\Domain\User\Ports\UserRepository; use SP\Domain\User\Services\UserMasterPass; use SP\Domain\User\Services\UserMasterPassStatus; @@ -62,7 +63,7 @@ class UserMasterPassTest extends UnitaryTestCase ->mutate(['isChangedPass' => false, 'lastUpdateMPass' => 10]); $userDataDto = new UserDataDto($user); - $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password(), $userDataDto); + $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password()); $this->configService ->expects($this->exactly(2)) @@ -80,7 +81,7 @@ class UserMasterPassTest extends UnitaryTestCase ->with($user->getMPass(), $user->getMKey(), $key) ->willReturn('a_master_pass'); - $out = $this->userMasterPass->load($userLoginDto); + $out = $this->userMasterPass->load($userLoginDto, $userDataDto); $this->assertEquals(UserMasterPassStatus::Ok, $out->getUserMasterPassStatus()); $this->assertEquals('a_master_pass', $out->getClearMasterPass()); @@ -98,7 +99,7 @@ class UserMasterPassTest extends UnitaryTestCase ->mutate(['isChangedPass' => false, 'lastUpdateMPass' => 10]); $userDataDto = new UserDataDto($user); - $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password(), $userDataDto); + $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password()); $this->configService ->expects($this->exactly(2)) @@ -116,7 +117,7 @@ class UserMasterPassTest extends UnitaryTestCase ->with($user->getMPass(), $user->getMKey(), $key) ->willReturn('a_master_pass'); - $out = $this->userMasterPass->load($userLoginDto, 'a_password'); + $out = $this->userMasterPass->load($userLoginDto, $userDataDto, 'a_password'); $this->assertEquals(UserMasterPassStatus::Ok, $out->getUserMasterPassStatus()); $this->assertEquals('a_master_pass', $out->getClearMasterPass()); @@ -129,6 +130,7 @@ class UserMasterPassTest extends UnitaryTestCase */ public function testLoadWithNotSet() { + $userDataDto = new UserDataDto(new User()); $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password()); $this->configService @@ -139,7 +141,7 @@ class UserMasterPassTest extends UnitaryTestCase ->expects($this->never()) ->method('decrypt'); - $out = $this->userMasterPass->load($userLoginDto); + $out = $this->userMasterPass->load($userLoginDto, $userDataDto); $this->assertEquals(UserMasterPassStatus::NotSet, $out->getUserMasterPassStatus()); } @@ -149,6 +151,7 @@ class UserMasterPassTest extends UnitaryTestCase */ public function testLoadWithNotSetAndEmptyPass() { + $userDataDto = new UserDataDto(new User(['use' => self::$faker->userName])); $userLoginDto = new UserLoginDto(self::$faker->userName()); $this->configService @@ -159,7 +162,7 @@ class UserMasterPassTest extends UnitaryTestCase ->expects($this->never()) ->method('decrypt'); - $out = $this->userMasterPass->load($userLoginDto); + $out = $this->userMasterPass->load($userLoginDto, $userDataDto); $this->assertEquals(UserMasterPassStatus::NotSet, $out->getUserMasterPassStatus()); } @@ -169,6 +172,7 @@ class UserMasterPassTest extends UnitaryTestCase */ public function testLoadWithNotSetAndEmptyUser() { + $userDataDto = new UserDataDto(new User(['pass' => self::$faker->password])); $userLoginDto = new UserLoginDto(); $this->configService @@ -179,7 +183,7 @@ class UserMasterPassTest extends UnitaryTestCase ->expects($this->never()) ->method('decrypt'); - $out = $this->userMasterPass->load($userLoginDto); + $out = $this->userMasterPass->load($userLoginDto, $userDataDto); $this->assertEquals(UserMasterPassStatus::NotSet, $out->getUserMasterPassStatus()); } @@ -194,7 +198,7 @@ class UserMasterPassTest extends UnitaryTestCase ->mutate(['isChangedPass' => false, 'lastUpdateMPass' => 10]); $userDataDto = new UserDataDto($user); - $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password(), $userDataDto); + $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password()); $this->configService ->expects($this->once()) @@ -205,7 +209,7 @@ class UserMasterPassTest extends UnitaryTestCase ->expects($this->never()) ->method('decrypt'); - $out = $this->userMasterPass->load($userLoginDto); + $out = $this->userMasterPass->load($userLoginDto, $userDataDto); $this->assertEquals(UserMasterPassStatus::NotSet, $out->getUserMasterPassStatus()); } @@ -220,7 +224,7 @@ class UserMasterPassTest extends UnitaryTestCase ->mutate(['isChangedPass' => false, 'lastUpdateMPass' => 0]); $userDataDto = new UserDataDto($user); - $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password(), $userDataDto); + $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password()); $this->configService ->expects($this->exactly(2)) @@ -228,15 +232,11 @@ class UserMasterPassTest extends UnitaryTestCase ->with(...self::withConsecutive(['masterPwd'], ['lastupdatempass'])) ->willReturn(Hash::hashKey('a_master_pass'), '5'); - $key = $userLoginDto->getLoginPass() . - $userLoginDto->getLoginUser() . - $this->config->getConfigData()->getPasswordSalt(); - $this->crypt ->expects($this->never()) ->method('decrypt'); - $out = $this->userMasterPass->load($userLoginDto); + $out = $this->userMasterPass->load($userLoginDto, $userDataDto); $this->assertEquals(UserMasterPassStatus::Changed, $out->getUserMasterPassStatus()); } @@ -251,7 +251,7 @@ class UserMasterPassTest extends UnitaryTestCase ->mutate(['isChangedPass' => true, 'lastUpdateMPass' => 10]); $userDataDto = new UserDataDto($user); - $userLoginDto = new UserLoginDto(self::$faker->userName(), null, $userDataDto); + $userLoginDto = new UserLoginDto(self::$faker->userName(), null); $this->configService ->expects($this->exactly(2)) @@ -259,15 +259,11 @@ class UserMasterPassTest extends UnitaryTestCase ->with(...self::withConsecutive(['masterPwd'], ['lastupdatempass'])) ->willReturn(Hash::hashKey('a_master_pass'), '5'); - $key = $userLoginDto->getLoginPass() . - $userLoginDto->getLoginUser() . - $this->config->getConfigData()->getPasswordSalt(); - $this->crypt ->expects($this->never()) ->method('decrypt'); - $out = $this->userMasterPass->load($userLoginDto); + $out = $this->userMasterPass->load($userLoginDto, $userDataDto); $this->assertEquals(UserMasterPassStatus::CheckOld, $out->getUserMasterPassStatus()); } @@ -282,7 +278,7 @@ class UserMasterPassTest extends UnitaryTestCase ->mutate(['isChangedPass' => false, 'lastUpdateMPass' => 10]); $userDataDto = new UserDataDto($user); - $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password(), $userDataDto); + $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password()); $this->configService ->expects($this->exactly(2)) @@ -290,16 +286,12 @@ class UserMasterPassTest extends UnitaryTestCase ->with(...self::withConsecutive(['masterPwd'], ['lastupdatempass'])) ->willReturn(Hash::hashKey('a_master_pass'), '5'); - $key = $userLoginDto->getLoginPass() . - $userLoginDto->getLoginUser() . - $this->config->getConfigData()->getPasswordSalt(); - $this->crypt ->expects($this->once()) ->method('decrypt') ->willThrowException(CryptException::error('test')); - $out = $this->userMasterPass->load($userLoginDto); + $out = $this->userMasterPass->load($userLoginDto, $userDataDto); $this->assertEquals(UserMasterPassStatus::CheckOld, $out->getUserMasterPassStatus()); } @@ -314,7 +306,7 @@ class UserMasterPassTest extends UnitaryTestCase ->mutate(['isChangedPass' => false, 'lastUpdateMPass' => 10]); $userDataDto = new UserDataDto($user); - $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password(), $userDataDto); + $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password()); $this->configService ->expects($this->exactly(2)) @@ -332,7 +324,7 @@ class UserMasterPassTest extends UnitaryTestCase ->with($user->getMPass(), $user->getMKey(), $key) ->willReturn('a_pass'); - $out = $this->userMasterPass->load($userLoginDto); + $out = $this->userMasterPass->load($userLoginDto, $userDataDto); $this->assertEquals(UserMasterPassStatus::Invalid, $out->getUserMasterPassStatus()); } @@ -347,7 +339,7 @@ class UserMasterPassTest extends UnitaryTestCase ->mutate(['isChangedPass' => false, 'lastUpdateMPass' => 10]); $userDataDto = new UserDataDto($user); - $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password(), $userDataDto); + $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password()); $this->configService ->expects($this->exactly(2)) @@ -355,10 +347,6 @@ class UserMasterPassTest extends UnitaryTestCase ->with(...self::withConsecutive(['masterPwd'], ['lastupdatempass'])) ->willReturn(Hash::hashKey('a_master_pass'), '5'); - $key = $userLoginDto->getLoginPass() . - $userLoginDto->getLoginUser() . - $this->config->getConfigData()->getPasswordSalt(); - $this->crypt ->expects($this->once()) ->method('decrypt') @@ -367,7 +355,7 @@ class UserMasterPassTest extends UnitaryTestCase $this->expectException(ServiceException::class); $this->expectExceptionMessage('test'); - $this->userMasterPass->load($userLoginDto); + $this->userMasterPass->load($userLoginDto, $userDataDto); } @@ -381,7 +369,7 @@ class UserMasterPassTest extends UnitaryTestCase ->mutate(['isChangedPass' => false, 'lastUpdateMPass' => 10]); $userDataDto = new UserDataDto($user); - $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password(), $userDataDto); + $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password()); $this->configService ->expects($this->exactly(3)) @@ -420,7 +408,7 @@ class UserMasterPassTest extends UnitaryTestCase ->method('updateMasterPassById') ->with($userDataDto->getId(), 'encrypted', 'a_secure_key'); - $out = $this->userMasterPass->updateFromOldPass('an_old_user_pass', $userLoginDto); + $out = $this->userMasterPass->updateFromOldPass('an_old_user_pass', $userLoginDto, $userDataDto); $this->assertEquals(UserMasterPassStatus::Ok, $out->getUserMasterPassStatus()); $this->assertEquals('encrypted', $out->getCryptMasterPass()); @@ -438,7 +426,7 @@ class UserMasterPassTest extends UnitaryTestCase ->mutate(['isChangedPass' => false, 'lastUpdateMPass' => 10]); $userDataDto = new UserDataDto($user); - $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password(), $userDataDto); + $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password()); $this->configService ->expects($this->exactly(2)) @@ -468,7 +456,7 @@ class UserMasterPassTest extends UnitaryTestCase ->expects($this->never()) ->method('updateMasterPassById'); - $out = $this->userMasterPass->updateFromOldPass('an_old_user_pass', $userLoginDto); + $out = $this->userMasterPass->updateFromOldPass('an_old_user_pass', $userLoginDto, $userDataDto); $this->assertEquals(UserMasterPassStatus::Invalid, $out->getUserMasterPassStatus()); } @@ -478,12 +466,7 @@ class UserMasterPassTest extends UnitaryTestCase */ public function testUpdateOnLogin() { - $user = UserDataGenerator::factory() - ->buildUserData() - ->mutate(['isChangedPass' => false, 'lastUpdateMPass' => 10]); - - $userDataDto = new UserDataDto($user); - $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password(), $userDataDto); + $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password()); $this->configService ->expects($this->once()) @@ -510,9 +493,9 @@ class UserMasterPassTest extends UnitaryTestCase $this->userRepository ->expects($this->once()) ->method('updateMasterPassById') - ->with($userDataDto->getId(), 'encrypted', 'a_secure_key'); + ->with(100, 'encrypted', 'a_secure_key'); - $out = $this->userMasterPass->updateOnLogin('a_master_pass', $userLoginDto); + $out = $this->userMasterPass->updateOnLogin('a_master_pass', $userLoginDto, 100); $this->assertEquals(UserMasterPassStatus::Ok, $out->getUserMasterPassStatus()); $this->assertEquals('encrypted', $out->getCryptMasterPass()); @@ -525,12 +508,7 @@ class UserMasterPassTest extends UnitaryTestCase */ public function testUpdateOnLoginWithSaveHash() { - $user = UserDataGenerator::factory() - ->buildUserData() - ->mutate(['isChangedPass' => false, 'lastUpdateMPass' => 10]); - - $userDataDto = new UserDataDto($user); - $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password(), $userDataDto); + $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password()); $this->configService ->expects($this->once()) @@ -567,9 +545,9 @@ class UserMasterPassTest extends UnitaryTestCase $this->userRepository ->expects($this->once()) ->method('updateMasterPassById') - ->with($userDataDto->getId(), 'encrypted', 'a_secure_key'); + ->with(100, 'encrypted', 'a_secure_key'); - $out = $this->userMasterPass->updateOnLogin('a_master_pass', $userLoginDto); + $out = $this->userMasterPass->updateOnLogin('a_master_pass', $userLoginDto, 100); $this->assertEquals(UserMasterPassStatus::Ok, $out->getUserMasterPassStatus()); $this->assertEquals('encrypted', $out->getCryptMasterPass()); @@ -582,12 +560,7 @@ class UserMasterPassTest extends UnitaryTestCase */ public function testUpdateOnLoginWithException() { - $user = UserDataGenerator::factory() - ->buildUserData() - ->mutate(['isChangedPass' => false, 'lastUpdateMPass' => 10]); - - $userDataDto = new UserDataDto($user); - $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password(), $userDataDto); + $userLoginDto = new UserLoginDto(self::$faker->userName(), self::$faker->password()); $this->configService ->expects($this->once()) @@ -629,7 +602,7 @@ class UserMasterPassTest extends UnitaryTestCase $this->expectException(ServiceException::class); $this->expectExceptionMessage('test'); - $this->userMasterPass->updateOnLogin('a_master_pass', $userLoginDto); + $this->userMasterPass->updateOnLogin('a_master_pass', $userLoginDto, 100); } /** diff --git a/tests/SPT/Providers/Auth/AuthProviderTest.php b/tests/SPT/Providers/Auth/AuthProviderTest.php index 506ff0c9..7f490742 100644 --- a/tests/SPT/Providers/Auth/AuthProviderTest.php +++ b/tests/SPT/Providers/Auth/AuthProviderTest.php @@ -28,10 +28,13 @@ use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\MockObject\Exception; use SP\Domain\Auth\Dtos\UserLoginDto; use SP\Domain\Auth\Services\AuthException; -use SP\Providers\Auth\AuthInterface; use SP\Providers\Auth\AuthProvider; -use SP\Providers\Auth\AuthTypeEnum; +use SP\Providers\Auth\AuthResult; +use SP\Providers\Auth\AuthService; +use SP\Providers\Auth\AuthType; use SP\Providers\Auth\Browser\BrowserAuthData; +use SP\Providers\Auth\Database\DatabaseAuthData; +use SP\Providers\Auth\Ldap\LdapAuthData; use SPT\UnitaryTestCase; /** @@ -50,14 +53,14 @@ class AuthProviderTest extends UnitaryTestCase */ public function testRegisterAuthFail() { - $auth1 = $this->createMock(AuthInterface::class); + $auth1 = $this->createMock(AuthService::class); - $this->authProvider->registerAuth($auth1, AuthTypeEnum::Ldap); + $this->authProvider->registerAuth($auth1, AuthType::Ldap); $this->expectException(AuthException::class); $this->expectExceptionMessage('Authentication already initialized'); - $this->authProvider->registerAuth($auth1, AuthTypeEnum::Ldap); + $this->authProvider->registerAuth($auth1, AuthType::Ldap); } /** @@ -76,19 +79,44 @@ class AuthProviderTest extends UnitaryTestCase $browserAuthData->setStatusCode(0); $browserAuthData->success(); - $auth = $this->createMock(AuthInterface::class); - $auth->expects(self::once()) - ->method('authenticate') - ->with($userLoginData) - ->willReturn($browserAuthData); + $ldapAuthData = new LdapAuthData(true); + $ldapAuthData->setName(self::$faker->name); + $ldapAuthData->setEmail(self::$faker->email); + $ldapAuthData->setStatusCode(1); + $ldapAuthData->success(); - $this->authProvider->registerAuth($auth, AuthTypeEnum::Ldap); + $databaseAuthData = new DatabaseAuthData(true); + $databaseAuthData->setName(self::$faker->name); + $databaseAuthData->setEmail(self::$faker->email); + $databaseAuthData->setStatusCode(2); + $databaseAuthData->success(); - $out = $this->authProvider->doAuth($userLoginData); + $authBrowser = $this->createMock(AuthService::class); + $authBrowser->expects(self::once()) + ->method('authenticate') + ->with($userLoginData) + ->willReturn($browserAuthData); - self::assertCount(1, $out); - self::assertEquals(AuthTypeEnum::Ldap->value, $out[0]->getAuthName()); - self::assertEquals($browserAuthData, $out[0]->getData()); + $authLdap = $this->createMock(AuthService::class); + $authLdap->expects(self::once()) + ->method('authenticate') + ->with($userLoginData) + ->willReturn($ldapAuthData); + + $authDatabase = $this->createMock(AuthService::class); + $authDatabase->expects(self::never()) + ->method('authenticate'); + + $this->authProvider->registerAuth($authBrowser, AuthType::Browser); + $this->authProvider->registerAuth($authLdap, AuthType::Ldap); + $this->authProvider->registerAuth($authDatabase, AuthType::Database); + + $callback = static function (AuthResult $authResult) { + $authData = $authResult->getAuthData(); + return $authData->isAuthoritative() && !$authData->isOk(); + }; + + $this->authProvider->doAuth($userLoginData, $callback); } protected function setUp(): void @@ -97,6 +125,4 @@ class AuthProviderTest extends UnitaryTestCase $this->authProvider = new AuthProvider($this->application); } - - } diff --git a/tests/SPT/UnitaryTestCase.php b/tests/SPT/UnitaryTestCase.php index 817f9093..6f7ba84b 100644 --- a/tests/SPT/UnitaryTestCase.php +++ b/tests/SPT/UnitaryTestCase.php @@ -84,7 +84,7 @@ abstract class UnitaryTestCase extends TestCase */ protected function setUp(): void { - $this->application = $this->mockApplication(); + $this->application = $this->buildApplication(); $this->config = $this->application->getConfig(); parent::setUp(); @@ -94,24 +94,34 @@ abstract class UnitaryTestCase extends TestCase * @return Application * @throws ContextException|Exception */ - private function mockApplication(): Application + private function buildApplication(): Application { - $this->context = new StatelessContext(); - $this->context->initialize(); - $this->context->setUserData($this->getUserDataDto()); - $this->context->setUserProfile(new ProfileData()); + $this->context = $this->buildContext(); return new Application( - $this->getConfig(), + $this->buildConfig(), $this->createStub(EventDispatcherInterface::class), $this->context ); } + /** + * @throws ContextException + */ + protected function buildContext(): ContextInterface + { + $context = new StatelessContext(); + $context->initialize(); + $context->setUserData($this->buildUserDataDto()); + $context->setUserProfile(new ProfileData()); + + return $context; + } + /** * @return UserDataDto */ - private function getUserDataDto(): UserDataDto + private function buildUserDataDto(): UserDataDto { return new UserDataDto( new User( @@ -129,7 +139,7 @@ abstract class UnitaryTestCase extends TestCase /** * @throws Exception */ - protected function getConfig(): ConfigFileService + protected function buildConfig(): ConfigFileService { $configData = ConfigDataGenerator::factory()->buildConfigData();