From 170582d4f5eef1cc53c043d67c5d9d07d097155c Mon Sep 17 00:00:00 2001 From: Christoph Wurst Date: Fri, 3 May 2019 15:09:19 +0200 Subject: [PATCH] Add a login chain to reduce the complexity of LoginController::tryLogin Signed-off-by: Christoph Wurst --- core/Controller/LoginController.php | 163 ++----- lib/composer/composer/autoload_classmap.php | 16 + lib/composer/composer/autoload_static.php | 16 + .../Authentication/Login/ALoginCommand.php | 47 ++ lib/private/Authentication/Login/Chain.php | 111 +++++ .../Login/ClearLostPasswordTokensCommand.php | 52 +++ .../Login/CompleteLoginCommand.php | 50 ++ .../Login/CreateSessionTokenCommand.php | 67 +++ .../Login/EmailLoginCommand.php | 61 +++ .../Login/FinishRememberedLoginCommand.php | 46 ++ .../Login/LoggedInCheckCommand.php | 53 +++ .../Authentication/Login/LoginData.php | 121 +++++ .../Authentication/Login/LoginResult.php | 83 ++++ .../Login/PreLoginHookCommand.php | 54 +++ .../Login/SetUserTimezoneCommand.php | 62 +++ .../Authentication/Login/TwoFactorCommand.php | 79 ++++ .../Authentication/Login/UidLoginCommand.php | 57 +++ .../UpdateLastPasswordConfirmCommand.php | 48 ++ .../Login/UserDisabledCheckCommand.php | 60 +++ lib/private/User/Manager.php | 2 +- tests/Core/Controller/LoginControllerTest.php | 437 +++++------------- .../Login/ALoginCommandTest.php | 122 +++++ .../ClearLostPasswordTokensCommandTest.php | 67 +++ .../Login/CompleteLoginCommandTest.php | 67 +++ .../Login/CreateSessionTokenCommandTest.php | 121 +++++ .../Login/EmailLoginCommandTest.php | 165 +++++++ .../FinishRememberedLoginCommandTest.php | 69 +++ .../Login/LoggedInCheckCommandTest.php | 67 +++ .../Login/PreLoginHookCommandTest.php | 66 +++ .../Login/SetUserTimezoneCommandTest.php | 90 ++++ .../Login/TwoFactorCommandTest.php | 179 +++++++ .../Login/UidLoginCommandTest.php | 80 ++++ .../UpdateLastPasswordConfirmCommandTest.php | 64 +++ .../Login/UserDisabledCheckCommandTest.php | 97 ++++ 34 files changed, 2485 insertions(+), 454 deletions(-) create mode 100644 lib/private/Authentication/Login/ALoginCommand.php create mode 100644 lib/private/Authentication/Login/Chain.php create mode 100644 lib/private/Authentication/Login/ClearLostPasswordTokensCommand.php create mode 100644 lib/private/Authentication/Login/CompleteLoginCommand.php create mode 100644 lib/private/Authentication/Login/CreateSessionTokenCommand.php create mode 100644 lib/private/Authentication/Login/EmailLoginCommand.php create mode 100644 lib/private/Authentication/Login/FinishRememberedLoginCommand.php create mode 100644 lib/private/Authentication/Login/LoggedInCheckCommand.php create mode 100644 lib/private/Authentication/Login/LoginData.php create mode 100644 lib/private/Authentication/Login/LoginResult.php create mode 100644 lib/private/Authentication/Login/PreLoginHookCommand.php create mode 100644 lib/private/Authentication/Login/SetUserTimezoneCommand.php create mode 100644 lib/private/Authentication/Login/TwoFactorCommand.php create mode 100644 lib/private/Authentication/Login/UidLoginCommand.php create mode 100644 lib/private/Authentication/Login/UpdateLastPasswordConfirmCommand.php create mode 100644 lib/private/Authentication/Login/UserDisabledCheckCommand.php create mode 100644 tests/lib/Authentication/Login/ALoginCommandTest.php create mode 100644 tests/lib/Authentication/Login/ClearLostPasswordTokensCommandTest.php create mode 100644 tests/lib/Authentication/Login/CompleteLoginCommandTest.php create mode 100644 tests/lib/Authentication/Login/CreateSessionTokenCommandTest.php create mode 100644 tests/lib/Authentication/Login/EmailLoginCommandTest.php create mode 100644 tests/lib/Authentication/Login/FinishRememberedLoginCommandTest.php create mode 100644 tests/lib/Authentication/Login/LoggedInCheckCommandTest.php create mode 100644 tests/lib/Authentication/Login/PreLoginHookCommandTest.php create mode 100644 tests/lib/Authentication/Login/SetUserTimezoneCommandTest.php create mode 100644 tests/lib/Authentication/Login/TwoFactorCommandTest.php create mode 100644 tests/lib/Authentication/Login/UidLoginCommandTest.php create mode 100644 tests/lib/Authentication/Login/UpdateLastPasswordConfirmCommandTest.php create mode 100644 tests/lib/Authentication/Login/UserDisabledCheckCommandTest.php diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php index 85d3b6b837f..9c11a37639a 100644 --- a/core/Controller/LoginController.php +++ b/core/Controller/LoginController.php @@ -33,7 +33,8 @@ namespace OC\Core\Controller; -use OC\Authentication\Token\IToken; +use OC\Authentication\Login\Chain; +use OC\Authentication\Login\LoginData; use OC\Authentication\TwoFactorAuth\Manager; use OC\Security\Bruteforce\Throttler; use OC\User\Session; @@ -44,17 +45,14 @@ use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\TemplateResponse; -use OCP\Authentication\TwoFactorAuth\IProvider; use OCP\Defaults; use OCP\IConfig; use OCP\ILogger; use OCP\IRequest; use OCP\ISession; use OCP\IURLGenerator; -use OCP\IUser; use OCP\IUserManager; use OCP\IUserSession; -use OC\Hooks\PublicEmitter; use OCP\Util; class LoginController extends Controller { @@ -74,27 +72,14 @@ class LoginController extends Controller { private $urlGenerator; /** @var ILogger */ private $logger; - /** @var Manager */ - private $twoFactorManager; /** @var Defaults */ private $defaults; /** @var Throttler */ private $throttler; + /** @var Chain */ + private $loginChain; - /** - * @param string $appName - * @param IRequest $request - * @param IUserManager $userManager - * @param IConfig $config - * @param ISession $session - * @param IUserSession $userSession - * @param IURLGenerator $urlGenerator - * @param ILogger $logger - * @param Manager $twoFactorManager - * @param Defaults $defaults - * @param Throttler $throttler - */ - public function __construct($appName, + public function __construct(?string $appName, IRequest $request, IUserManager $userManager, IConfig $config, @@ -102,9 +87,9 @@ class LoginController extends Controller { IUserSession $userSession, IURLGenerator $urlGenerator, ILogger $logger, - Manager $twoFactorManager, Defaults $defaults, - Throttler $throttler) { + Throttler $throttler, + Chain $loginChain) { parent::__construct($appName, $request); $this->userManager = $userManager; $this->config = $config; @@ -112,9 +97,9 @@ class LoginController extends Controller { $this->userSession = $userSession; $this->urlGenerator = $urlGenerator; $this->logger = $logger; - $this->twoFactorManager = $twoFactorManager; $this->defaults = $defaults; $this->throttler = $throttler; + $this->loginChain = $loginChain; } /** @@ -226,8 +211,8 @@ class LoginController extends Controller { * @param array $parameters * @return array */ - private function setPasswordResetParameters( - string $user = null, array $parameters): array { + private function setPasswordResetParameters(?string $user, + array $parameters): array { if ($user !== null && $user !== '') { $userObj = $this->userManager->get($user); } else { @@ -250,12 +235,8 @@ class LoginController extends Controller { return $parameters; } - /** - * @param string $redirectUrl - * @return RedirectResponse - */ - private function generateRedirect($redirectUrl) { - if (!is_null($redirectUrl) && $this->userSession->isLoggedIn()) { + private function generateRedirect(?string $redirectUrl): RedirectResponse { + if ($redirectUrl !== null && $this->userSession->isLoggedIn()) { $location = $this->urlGenerator->getAbsoluteURL(urldecode($redirectUrl)); // Deny the redirect if the URL contains a @ // This prevents unvalidated redirects like ?redirect_url=:user@domain.com @@ -275,16 +256,15 @@ class LoginController extends Controller { * @param string $user * @param string $password * @param string $redirect_url - * @param boolean $remember_login * @param string $timezone * @param string $timezone_offset * @return RedirectResponse */ - public function tryLogin($user, $password, $redirect_url, $remember_login = true, $timezone = '', $timezone_offset = '') { - if(!is_string($user)) { - throw new \InvalidArgumentException('Username must be string'); - } - + public function tryLogin(string $user, + string $password, + string $redirect_url = null, + string $timezone = '', + string $timezone_offset = ''): RedirectResponse { // If the user is already logged in and the CSRF check does not pass then // simply redirect the user to the correct page as required. This is the // case when an user has already logged-in, in another tab. @@ -292,96 +272,27 @@ class LoginController extends Controller { return $this->generateRedirect($redirect_url); } - if ($this->userManager instanceof PublicEmitter) { - $this->userManager->emit('\OC\User', 'preLogin', array($user, $password)); - } - - $originalUser = $user; - - $userObj = $this->userManager->get($user); - - if ($userObj !== null && $userObj->isEnabled() === false) { - $this->logger->warning('Login failed: \''. $user . '\' disabled' . - ' (Remote IP: \''. $this->request->getRemoteAddress(). '\')', - ['app' => 'core']); - return $this->createLoginFailedResponse($user, $originalUser, - $redirect_url, self::LOGIN_MSG_USERDISABLED); - } - - // TODO: Add all the insane error handling - /* @var $loginResult IUser */ - $loginResult = $this->userManager->checkPasswordNoLogging($user, $password); - if ($loginResult === false) { - $users = $this->userManager->getByEmail($user); - // we only allow login by email if unique - if (count($users) === 1) { - $previousUser = $user; - $user = $users[0]->getUID(); - if($user !== $previousUser) { - $loginResult = $this->userManager->checkPassword($user, $password); - } - } - } - - if ($loginResult === false) { - $this->logger->warning('Login failed: \''. $user . - '\' (Remote IP: \''. $this->request->getRemoteAddress(). '\')', - ['app' => 'core']); - return $this->createLoginFailedResponse($user, $originalUser, - $redirect_url, self::LOGIN_MSG_INVALIDPASSWORD); - } - - // TODO: remove password checks from above and let the user session handle failures - // requires https://github.com/owncloud/core/pull/24616 - $this->userSession->completeLogin($loginResult, ['loginName' => $user, 'password' => $password]); - - $tokenType = IToken::REMEMBER; - if ((int)$this->config->getSystemValue('remember_login_cookie_lifetime', 60*60*24*15) === 0) { - $remember_login = false; - $tokenType = IToken::DO_NOT_REMEMBER; - } - - $this->userSession->createSessionToken($this->request, $loginResult->getUID(), $user, $password, $tokenType); - $this->userSession->updateTokens($loginResult->getUID(), $password); - - // User has successfully logged in, now remove the password reset link, when it is available - $this->config->deleteUserValue($loginResult->getUID(), 'core', 'lostpassword'); - - $this->session->set('last-password-confirm', $loginResult->getLastLogin()); - - if ($timezone_offset !== '') { - $this->config->setUserValue($loginResult->getUID(), 'core', 'timezone', $timezone); - $this->session->set('timezone', $timezone_offset); - } - - if ($this->twoFactorManager->isTwoFactorAuthenticated($loginResult)) { - $this->twoFactorManager->prepareTwoFactorLogin($loginResult, $remember_login); - - $providers = $this->twoFactorManager->getProviderSet($loginResult)->getPrimaryProviders(); - if (count($providers) === 1) { - // Single provider, hence we can redirect to that provider's challenge page directly - /* @var $provider IProvider */ - $provider = array_pop($providers); - $url = 'core.TwoFactorChallenge.showChallenge'; - $urlParams = [ - 'challengeProviderId' => $provider->getId(), - ]; - } else { - $url = 'core.TwoFactorChallenge.selectChallenge'; - $urlParams = []; - } - - if (!is_null($redirect_url)) { - $urlParams['redirect_url'] = $redirect_url; - } - - return new RedirectResponse($this->urlGenerator->linkToRoute($url, $urlParams)); + $data = new LoginData( + $this->request, + $user, + $password, + $redirect_url, + $timezone, + $timezone_offset + ); + $result = $this->loginChain->process($data); + if (!$result->isSuccess()) { + return $this->createLoginFailedResponse( + $data->getUsername(), + $user, + $redirect_url, + $result->getErrorMessage() + ); } - if ($remember_login) { - $this->userSession->createRememberMeToken($loginResult); + if ($result->getRedirectUrl() !== null) { + return new RedirectResponse($result->getRedirectUrl()); } - return $this->generateRedirect($redirect_url); } @@ -398,8 +309,8 @@ class LoginController extends Controller { $user, $originalUser, $redirect_url, string $loginMessage) { // Read current user and append if possible we need to // return the unmodified user otherwise we will leak the login name - $args = !is_null($user) ? ['user' => $originalUser] : []; - if (!is_null($redirect_url)) { + $args = $user !== null ? ['user' => $originalUser] : []; + if ($redirect_url !== null) { $args['redirect_url'] = $redirect_url; } $response = new RedirectResponse( diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index cad7c21a594..e47cfe26b72 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -506,6 +506,22 @@ return array( 'OC\\Authentication\\Exceptions\\UserAlreadyLoggedInException' => $baseDir . '/lib/private/Authentication/Exceptions/UserAlreadyLoggedInException.php', 'OC\\Authentication\\LoginCredentials\\Credentials' => $baseDir . '/lib/private/Authentication/LoginCredentials/Credentials.php', 'OC\\Authentication\\LoginCredentials\\Store' => $baseDir . '/lib/private/Authentication/LoginCredentials/Store.php', + 'OC\\Authentication\\Login\\ALoginCommand' => $baseDir . '/lib/private/Authentication/Login/ALoginCommand.php', + 'OC\\Authentication\\Login\\Chain' => $baseDir . '/lib/private/Authentication/Login/Chain.php', + 'OC\\Authentication\\Login\\ClearLostPasswordTokensCommand' => $baseDir . '/lib/private/Authentication/Login/ClearLostPasswordTokensCommand.php', + 'OC\\Authentication\\Login\\CompleteLoginCommand' => $baseDir . '/lib/private/Authentication/Login/CompleteLoginCommand.php', + 'OC\\Authentication\\Login\\CreateSessionTokenCommand' => $baseDir . '/lib/private/Authentication/Login/CreateSessionTokenCommand.php', + 'OC\\Authentication\\Login\\EmailLoginCommand' => $baseDir . '/lib/private/Authentication/Login/EmailLoginCommand.php', + 'OC\\Authentication\\Login\\FinishRememberedLoginCommand' => $baseDir . '/lib/private/Authentication/Login/FinishRememberedLoginCommand.php', + 'OC\\Authentication\\Login\\LoggedInCheckCommand' => $baseDir . '/lib/private/Authentication/Login/LoggedInCheckCommand.php', + 'OC\\Authentication\\Login\\LoginData' => $baseDir . '/lib/private/Authentication/Login/LoginData.php', + 'OC\\Authentication\\Login\\LoginResult' => $baseDir . '/lib/private/Authentication/Login/LoginResult.php', + 'OC\\Authentication\\Login\\PreLoginHookCommand' => $baseDir . '/lib/private/Authentication/Login/PreLoginHookCommand.php', + 'OC\\Authentication\\Login\\SetUserTimezoneCommand' => $baseDir . '/lib/private/Authentication/Login/SetUserTimezoneCommand.php', + 'OC\\Authentication\\Login\\TwoFactorCommand' => $baseDir . '/lib/private/Authentication/Login/TwoFactorCommand.php', + 'OC\\Authentication\\Login\\UidLoginCommand' => $baseDir . '/lib/private/Authentication/Login/UidLoginCommand.php', + 'OC\\Authentication\\Login\\UpdateLastPasswordConfirmCommand' => $baseDir . '/lib/private/Authentication/Login/UpdateLastPasswordConfirmCommand.php', + 'OC\\Authentication\\Login\\UserDisabledCheckCommand' => $baseDir . '/lib/private/Authentication/Login/UserDisabledCheckCommand.php', 'OC\\Authentication\\Token\\DefaultToken' => $baseDir . '/lib/private/Authentication/Token/DefaultToken.php', 'OC\\Authentication\\Token\\DefaultTokenCleanupJob' => $baseDir . '/lib/private/Authentication/Token/DefaultTokenCleanupJob.php', 'OC\\Authentication\\Token\\DefaultTokenMapper' => $baseDir . '/lib/private/Authentication/Token/DefaultTokenMapper.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 7a49d254e90..d5d783aa114 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -536,6 +536,22 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Authentication\\Exceptions\\UserAlreadyLoggedInException' => __DIR__ . '/../../..' . '/lib/private/Authentication/Exceptions/UserAlreadyLoggedInException.php', 'OC\\Authentication\\LoginCredentials\\Credentials' => __DIR__ . '/../../..' . '/lib/private/Authentication/LoginCredentials/Credentials.php', 'OC\\Authentication\\LoginCredentials\\Store' => __DIR__ . '/../../..' . '/lib/private/Authentication/LoginCredentials/Store.php', + 'OC\\Authentication\\Login\\ALoginCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/ALoginCommand.php', + 'OC\\Authentication\\Login\\Chain' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/Chain.php', + 'OC\\Authentication\\Login\\ClearLostPasswordTokensCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/ClearLostPasswordTokensCommand.php', + 'OC\\Authentication\\Login\\CompleteLoginCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/CompleteLoginCommand.php', + 'OC\\Authentication\\Login\\CreateSessionTokenCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/CreateSessionTokenCommand.php', + 'OC\\Authentication\\Login\\EmailLoginCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/EmailLoginCommand.php', + 'OC\\Authentication\\Login\\FinishRememberedLoginCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/FinishRememberedLoginCommand.php', + 'OC\\Authentication\\Login\\LoggedInCheckCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/LoggedInCheckCommand.php', + 'OC\\Authentication\\Login\\LoginData' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/LoginData.php', + 'OC\\Authentication\\Login\\LoginResult' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/LoginResult.php', + 'OC\\Authentication\\Login\\PreLoginHookCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/PreLoginHookCommand.php', + 'OC\\Authentication\\Login\\SetUserTimezoneCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/SetUserTimezoneCommand.php', + 'OC\\Authentication\\Login\\TwoFactorCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/TwoFactorCommand.php', + 'OC\\Authentication\\Login\\UidLoginCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/UidLoginCommand.php', + 'OC\\Authentication\\Login\\UpdateLastPasswordConfirmCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/UpdateLastPasswordConfirmCommand.php', + 'OC\\Authentication\\Login\\UserDisabledCheckCommand' => __DIR__ . '/../../..' . '/lib/private/Authentication/Login/UserDisabledCheckCommand.php', 'OC\\Authentication\\Token\\DefaultToken' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/DefaultToken.php', 'OC\\Authentication\\Token\\DefaultTokenCleanupJob' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/DefaultTokenCleanupJob.php', 'OC\\Authentication\\Token\\DefaultTokenMapper' => __DIR__ . '/../../..' . '/lib/private/Authentication/Token/DefaultTokenMapper.php', diff --git a/lib/private/Authentication/Login/ALoginCommand.php b/lib/private/Authentication/Login/ALoginCommand.php new file mode 100644 index 00000000000..368cdaf4913 --- /dev/null +++ b/lib/private/Authentication/Login/ALoginCommand.php @@ -0,0 +1,47 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OC\Authentication\Login; + +abstract class ALoginCommand { + + /** @var ALoginCommand */ + protected $next; + + public function setNext(ALoginCommand $next): ALoginCommand { + $this->next = $next; + return $next; + } + + protected function processNextOrFinishSuccessfully(LoginData $loginData): LoginResult { + if ($this->next !== null) { + return $this->next->process($loginData); + } else { + return LoginResult::success($loginData); + } + } + + public abstract function process(LoginData $loginData): LoginResult; + +} diff --git a/lib/private/Authentication/Login/Chain.php b/lib/private/Authentication/Login/Chain.php new file mode 100644 index 00000000000..c8e82935883 --- /dev/null +++ b/lib/private/Authentication/Login/Chain.php @@ -0,0 +1,111 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OC\Authentication\Login; + +class Chain { + + /** @var PreLoginHookCommand */ + private $preLoginHookCommand; + + /** @var UserDisabledCheckCommand */ + private $userDisabledCheckCommand; + + /** @var UidLoginCommand */ + private $uidLoginCommand; + + /** @var EmailLoginCommand */ + private $emailLoginCommand; + + /** @var LoggedInCheckCommand */ + private $loggedInCheckCommand; + + /** @var CompleteLoginCommand */ + private $completeLoginCommand; + + /** @var CreateSessionTokenCommand */ + private $createSessionTokenCommand; + + /** @var ClearLostPasswordTokensCommand */ + private $clearLostPasswordTokensCommand; + + /** @var UpdateLastPasswordConfirmCommand */ + private $updateLastPasswordConfirmCommand; + + /** @var SetUserTimezoneCommand */ + private $setUserTimezoneCommand; + + /** @var TwoFactorCommand */ + private $twoFactorCommand; + + /** @var FinishRememberedLoginCommand */ + private $finishRememberedLoginCommand; + + public function __construct(PreLoginHookCommand $preLoginHookCommand, + UserDisabledCheckCommand $userDisabledCheckCommand, + UidLoginCommand $uidLoginCommand, + EmailLoginCommand $emailLoginCommand, + LoggedInCheckCommand $loggedInCheckCommand, + CompleteLoginCommand $completeLoginCommand, + CreateSessionTokenCommand $createSessionTokenCommand, + ClearLostPasswordTokensCommand $clearLostPasswordTokensCommand, + UpdateLastPasswordConfirmCommand $updateLastPasswordConfirmCommand, + SetUserTimezoneCommand $setUserTimezoneCommand, + TwoFactorCommand $twoFactorCommand, + FinishRememberedLoginCommand $finishRememberedLoginCommand + ) { + $this->preLoginHookCommand = $preLoginHookCommand; + $this->userDisabledCheckCommand = $userDisabledCheckCommand; + $this->uidLoginCommand = $uidLoginCommand; + $this->emailLoginCommand = $emailLoginCommand; + $this->loggedInCheckCommand = $loggedInCheckCommand; + $this->completeLoginCommand = $completeLoginCommand; + $this->createSessionTokenCommand = $createSessionTokenCommand; + $this->clearLostPasswordTokensCommand = $clearLostPasswordTokensCommand; + $this->updateLastPasswordConfirmCommand = $updateLastPasswordConfirmCommand; + $this->setUserTimezoneCommand = $setUserTimezoneCommand; + $this->twoFactorCommand = $twoFactorCommand; + $this->finishRememberedLoginCommand = $finishRememberedLoginCommand; + } + + public function process(LoginData $loginData): LoginResult { + $chain = $this->preLoginHookCommand; + $chain + ->setNext($this->userDisabledCheckCommand) + ->setNext($this->uidLoginCommand) + ->setNext($this->emailLoginCommand) + ->setNext($this->loggedInCheckCommand) + ->setNext($this->completeLoginCommand) + ->setNext($this->createSessionTokenCommand) + ->setNext($this->clearLostPasswordTokensCommand) + ->setNext($this->updateLastPasswordConfirmCommand) + ->setNext($this->setUserTimezoneCommand) + ->setNext($this->twoFactorCommand) + ->setNext($this->finishRememberedLoginCommand); + + return $chain->process($loginData); + } + +} diff --git a/lib/private/Authentication/Login/ClearLostPasswordTokensCommand.php b/lib/private/Authentication/Login/ClearLostPasswordTokensCommand.php new file mode 100644 index 00000000000..77fddd5c762 --- /dev/null +++ b/lib/private/Authentication/Login/ClearLostPasswordTokensCommand.php @@ -0,0 +1,52 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OC\Authentication\Login; + +use OCP\IConfig; + +class ClearLostPasswordTokensCommand extends ALoginCommand { + + /** @var IConfig */ + private $config; + + public function __construct(IConfig $config) { + $this->config = $config; + } + + /** + * User has successfully logged in, now remove the password reset link, when it is available + */ + public function process(LoginData $loginData): LoginResult { + $this->config->deleteUserValue( + $loginData->getUser()->getUID(), + 'core', + 'lostpassword' + ); + + return $this->processNextOrFinishSuccessfully($loginData); + } + +} diff --git a/lib/private/Authentication/Login/CompleteLoginCommand.php b/lib/private/Authentication/Login/CompleteLoginCommand.php new file mode 100644 index 00000000000..84f96c2789c --- /dev/null +++ b/lib/private/Authentication/Login/CompleteLoginCommand.php @@ -0,0 +1,50 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OC\Authentication\Login; + +use OC\User\Session; + +class CompleteLoginCommand extends ALoginCommand { + + /** @var Session */ + private $userSession; + + public function __construct(Session $userSession) { + $this->userSession = $userSession; + } + + public function process(LoginData $loginData): LoginResult { + $this->userSession->completeLogin( + $loginData->getUser(), + [ + 'loginName' => $loginData->getUsername(), + 'password' => $loginData->getPassword(), + ] + ); + + return $this->processNextOrFinishSuccessfully($loginData); + } + +} diff --git a/lib/private/Authentication/Login/CreateSessionTokenCommand.php b/lib/private/Authentication/Login/CreateSessionTokenCommand.php new file mode 100644 index 00000000000..14ad6d18b30 --- /dev/null +++ b/lib/private/Authentication/Login/CreateSessionTokenCommand.php @@ -0,0 +1,67 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OC\Authentication\Login; + +use OC\Authentication\Token\IToken; +use OC\User\Session; +use OCP\IConfig; + +class CreateSessionTokenCommand extends ALoginCommand { + + /** @var IConfig */ + private $config; + + /** @var Session */ + private $userSession; + + public function __construct(IConfig $config, + Session $userSession) { + $this->config = $config; + $this->userSession = $userSession; + } + + public function process(LoginData $loginData): LoginResult { + $tokenType = IToken::REMEMBER; + if ((int)$this->config->getSystemValue('remember_login_cookie_lifetime', 60 * 60 * 24 * 15) === 0) { + $loginData->setRememberLogin(false); + $tokenType = IToken::DO_NOT_REMEMBER; + } + + $this->userSession->createSessionToken( + $loginData->getRequest(), + $loginData->getUser()->getUID(), + $loginData->getUsername(), + $loginData->getPassword(), + $tokenType + ); + $this->userSession->updateTokens( + $loginData->getUser()->getUID(), + $loginData->getUsername() + ); + + return $this->processNextOrFinishSuccessfully($loginData); + } +} diff --git a/lib/private/Authentication/Login/EmailLoginCommand.php b/lib/private/Authentication/Login/EmailLoginCommand.php new file mode 100644 index 00000000000..90d226dcc63 --- /dev/null +++ b/lib/private/Authentication/Login/EmailLoginCommand.php @@ -0,0 +1,61 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OC\Authentication\Login; + +use OCP\IUserManager; + +class EmailLoginCommand extends ALoginCommand { + + /** @var IUserManager */ + private $userManager; + + public function __construct(IUserManager $userManager) { + $this->userManager = $userManager; + } + + public function process(LoginData $loginData): LoginResult { + if ($loginData->getUser() === false) { + $users = $this->userManager->getByEmail($loginData->getUsername()); + // we only allow login by email if unique + if (count($users) === 1) { + $username = $users[0]->getUID(); + if ($username !== $loginData->getUsername()) { + $user = $this->userManager->checkPassword( + $username, + $loginData->getPassword() + ); + if ($user !== false) { + $loginData->setUser($user); + $loginData->setUsername($username); + } + } + } + } + + return $this->processNextOrFinishSuccessfully($loginData); + } + +} diff --git a/lib/private/Authentication/Login/FinishRememberedLoginCommand.php b/lib/private/Authentication/Login/FinishRememberedLoginCommand.php new file mode 100644 index 00000000000..84f387a0ce3 --- /dev/null +++ b/lib/private/Authentication/Login/FinishRememberedLoginCommand.php @@ -0,0 +1,46 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OC\Authentication\Login; + +use OC\User\Session; + +class FinishRememberedLoginCommand extends ALoginCommand { + + /** @var Session */ + private $userSession; + + public function __construct(Session $userSession) { + $this->userSession = $userSession; + } + + public function process(LoginData $loginData): LoginResult { + if ($loginData->isRememberLogin()) { + $this->userSession->createRememberMeToken($loginData->getUser()); + } + + return $this->processNextOrFinishSuccessfully($loginData); + } + +} diff --git a/lib/private/Authentication/Login/LoggedInCheckCommand.php b/lib/private/Authentication/Login/LoggedInCheckCommand.php new file mode 100644 index 00000000000..3f70385ea21 --- /dev/null +++ b/lib/private/Authentication/Login/LoggedInCheckCommand.php @@ -0,0 +1,53 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OC\Authentication\Login; + +use OC\Core\Controller\LoginController; +use OCP\ILogger; + +class LoggedInCheckCommand extends ALoginCommand { + + /** @var ILogger */ + private $logger; + + public function __construct(ILogger $logger) { + $this->logger = $logger; + } + + public function process(LoginData $loginData): LoginResult { + if ($loginData->getUser() === false) { + $username = $loginData->getUsername(); + $ip = $loginData->getRequest()->getRemoteAddress(); + + $this->logger->warning("Login failed: $username (Remote IP: $ip)"); + + return LoginResult::failure($loginData, LoginController::LOGIN_MSG_INVALIDPASSWORD); + } + + return $this->processNextOrFinishSuccessfully($loginData); + } + +} diff --git a/lib/private/Authentication/Login/LoginData.php b/lib/private/Authentication/Login/LoginData.php new file mode 100644 index 00000000000..a1e8ff12916 --- /dev/null +++ b/lib/private/Authentication/Login/LoginData.php @@ -0,0 +1,121 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OC\Authentication\Login; + +use OCP\IRequest; +use OCP\IUser; + +class LoginData { + + /** @var IRequest */ + private $request; + + /** @var string */ + private $username; + + /** @var string */ + private $password; + + /** @var string */ + private $redirectUrl; + + /** @var string */ + private $timeZone; + + /** @var string */ + private $timeZoneOffset; + + /** @var IUser|false|null */ + private $user = null; + + /** @var bool */ + private $rememberLogin = true; + + public function __construct(IRequest $request, + string $username, + string $password, + string $redirectUrl = null, + string $timeZone = '', + string $timeZoneOffset = '') { + $this->request = $request; + $this->username = $username; + $this->password = $password; + $this->redirectUrl = $redirectUrl; + $this->timeZone = $timeZone; + $this->timeZoneOffset = $timeZoneOffset; + } + + public function getRequest(): IRequest { + return $this->request; + } + + public function setUsername(string $username): void { + $this->username = $username; + } + + public function getUsername(): string { + return $this->username; + } + + public function getPassword(): string { + return $this->password; + } + + public function getRedirectUrl(): ?string { + return $this->redirectUrl; + } + + public function getTimeZone(): string { + return $this->timeZone; + } + + public function getTimeZoneOffset(): string { + return $this->timeZoneOffset; + } + + /** + * @param IUser|false|null $user + */ + public function setUser($user) { + $this->user = $user; + } + + /** + * @return false|IUser|null + */ + public function getUser() { + return $this->user; + } + + public function setRememberLogin(bool $rememberLogin): void { + $this->rememberLogin = $rememberLogin; + } + + public function isRememberLogin(): bool { + return $this->rememberLogin; + } + +} diff --git a/lib/private/Authentication/Login/LoginResult.php b/lib/private/Authentication/Login/LoginResult.php new file mode 100644 index 00000000000..e2eed344560 --- /dev/null +++ b/lib/private/Authentication/Login/LoginResult.php @@ -0,0 +1,83 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OC\Authentication\Login; + +class LoginResult { + + /** @var bool */ + private $success; + + /** @var LoginData */ + private $loginData; + + /** @var string|null */ + private $redirectUrl; + + /** @var string|null */ + private $errorMessage; + + private function __construct(bool $success, LoginData $loginData) { + $this->success = $success; + $this->loginData = $loginData; + } + + private function setRedirectUrl(string $url) { + $this->redirectUrl = $url; + } + + private function setErrorMessage(string $msg) { + $this->errorMessage = $msg; + } + + public static function success(LoginData $data, ?string $redirectUrl = null) { + $result = new static(true, $data); + if ($redirectUrl !== null) { + $result->setRedirectUrl($redirectUrl); + } + return $result; + } + + public static function failure(LoginData $data, string $msg = null): LoginResult { + $result = new static(false, $data); + if ($msg !== null) { + $result->setErrorMessage($msg); + } + return $result; + } + + public function isSuccess(): bool { + return $this->success; + } + + public function getRedirectUrl(): ?string { + return $this->redirectUrl; + } + + public function getErrorMessage(): ?string { + return $this->errorMessage; + } + +} diff --git a/lib/private/Authentication/Login/PreLoginHookCommand.php b/lib/private/Authentication/Login/PreLoginHookCommand.php new file mode 100644 index 00000000000..87c52c6fef1 --- /dev/null +++ b/lib/private/Authentication/Login/PreLoginHookCommand.php @@ -0,0 +1,54 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OC\Authentication\Login; + +use OC\Hooks\PublicEmitter; +use OCP\IUserManager; + +class PreLoginHookCommand extends ALoginCommand { + + /** @var IUserManager */ + private $userManager; + + public function __construct(IUserManager $userManager) { + $this->userManager = $userManager; + } + + public function process(LoginData $loginData): LoginResult { + if ($this->userManager instanceof PublicEmitter) { + $this->userManager->emit( + '\OC\User', + 'preLogin', + [ + $loginData->getUsername(), + $loginData->getPassword(), + ] + ); + } + + return $this->processNextOrFinishSuccessfully($loginData); + } +} \ No newline at end of file diff --git a/lib/private/Authentication/Login/SetUserTimezoneCommand.php b/lib/private/Authentication/Login/SetUserTimezoneCommand.php new file mode 100644 index 00000000000..0c741272c9f --- /dev/null +++ b/lib/private/Authentication/Login/SetUserTimezoneCommand.php @@ -0,0 +1,62 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OC\Authentication\Login; + +use OCP\IConfig; +use OCP\ISession; + +class SetUserTimezoneCommand extends ALoginCommand { + + /** @var IConfig */ + private $config; + + /** @var ISession */ + private $session; + + public function __construct(IConfig $config, + ISession $session) { + $this->config = $config; + $this->session = $session; + } + + public function process(LoginData $loginData): LoginResult { + if ($loginData->getTimeZoneOffset() !== '') { + $this->config->setUserValue( + $loginData->getUser()->getUID(), + 'core', + 'timezone', + $loginData->getTimeZone() + ); + $this->session->set( + 'timezone', + $loginData->getTimeZoneOffset() + ); + } + + return $this->processNextOrFinishSuccessfully($loginData); + } + +} diff --git a/lib/private/Authentication/Login/TwoFactorCommand.php b/lib/private/Authentication/Login/TwoFactorCommand.php new file mode 100644 index 00000000000..2825dc1763f --- /dev/null +++ b/lib/private/Authentication/Login/TwoFactorCommand.php @@ -0,0 +1,79 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OC\Authentication\Login; + +use function array_pop; +use function count; +use OC\Authentication\TwoFactorAuth\Manager; +use OCP\Authentication\TwoFactorAuth\IProvider; +use OCP\IURLGenerator; + +class TwoFactorCommand extends ALoginCommand { + + /** @var Manager */ + private $twoFactorManager; + + /** @var IURLGenerator */ + private $urlGenerator; + + public function __construct(Manager $twoFactorManager, + IURLGenerator $urlGenerator) { + $this->twoFactorManager = $twoFactorManager; + $this->urlGenerator = $urlGenerator; + } + + public function process(LoginData $loginData): LoginResult { + if (!$this->twoFactorManager->isTwoFactorAuthenticated($loginData->getUser())) { + return $this->processNextOrFinishSuccessfully($loginData); + } + + $this->twoFactorManager->prepareTwoFactorLogin($loginData->getUser(), $loginData->isRememberLogin()); + + $providers = $this->twoFactorManager->getProviderSet($loginData->getUser())->getPrimaryProviders(); + if (count($providers) === 1) { + // Single provider, hence we can redirect to that provider's challenge page directly + /* @var $provider IProvider */ + $provider = array_pop($providers); + $url = 'core.TwoFactorChallenge.showChallenge'; + $urlParams = [ + 'challengeProviderId' => $provider->getId(), + ]; + } else { + $url = 'core.TwoFactorChallenge.selectChallenge'; + $urlParams = []; + } + + if ($loginData->getRedirectUrl() !== null) { + $urlParams['redirect_url'] = $loginData->getRedirectUrl(); + } + + return LoginResult::success( + $loginData, + $this->urlGenerator->linkToRoute($url, $urlParams) + ); + } + +} diff --git a/lib/private/Authentication/Login/UidLoginCommand.php b/lib/private/Authentication/Login/UidLoginCommand.php new file mode 100644 index 00000000000..345128440df --- /dev/null +++ b/lib/private/Authentication/Login/UidLoginCommand.php @@ -0,0 +1,57 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OC\Authentication\Login; + +use OC\User\Manager; +use OCP\IUser; + +class UidLoginCommand extends ALoginCommand { + + /** @var Manager */ + private $userManager; + + public function __construct(Manager $userManager) { + $this->userManager = $userManager; + } + + /** + * @param LoginData $loginData + * + * @return LoginResult + */ + public function process(LoginData $loginData): LoginResult { + /* @var $loginResult IUser */ + $user = $this->userManager->checkPasswordNoLogging( + $loginData->getUsername(), + $loginData->getPassword() + ); + + $loginData->setUser($user); + + return $this->processNextOrFinishSuccessfully($loginData); + } + +} diff --git a/lib/private/Authentication/Login/UpdateLastPasswordConfirmCommand.php b/lib/private/Authentication/Login/UpdateLastPasswordConfirmCommand.php new file mode 100644 index 00000000000..b4ae968bbc1 --- /dev/null +++ b/lib/private/Authentication/Login/UpdateLastPasswordConfirmCommand.php @@ -0,0 +1,48 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OC\Authentication\Login; + +use OCP\ISession; + +class UpdateLastPasswordConfirmCommand extends ALoginCommand { + + /** @var ISession */ + private $session; + + public function __construct(ISession $session) { + $this->session = $session; + } + + public function process(LoginData $loginData): LoginResult { + $this->session->set( + 'last-password-confirm', + $loginData->getUser()->getLastLogin() + ); + + return $this->processNextOrFinishSuccessfully($loginData); + } + +} diff --git a/lib/private/Authentication/Login/UserDisabledCheckCommand.php b/lib/private/Authentication/Login/UserDisabledCheckCommand.php new file mode 100644 index 00000000000..a37244edf39 --- /dev/null +++ b/lib/private/Authentication/Login/UserDisabledCheckCommand.php @@ -0,0 +1,60 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace OC\Authentication\Login; + +use OC\Core\Controller\LoginController; +use OCP\ILogger; +use OCP\IUserManager; + +class UserDisabledCheckCommand extends ALoginCommand { + + /** @var IUserManager */ + private $userManager; + + /** @var ILogger */ + private $logger; + + public function __construct(IUserManager $userManager, + ILogger $logger) { + $this->userManager = $userManager; + $this->logger = $logger; + } + + public function process(LoginData $loginData): LoginResult { + $user = $this->userManager->get($loginData->getUsername()); + if ($user !== null && $user->isEnabled() === false) { + $username = $loginData->getUsername(); + $ip = $loginData->getRequest()->getRemoteAddress(); + + $this->logger->warning("Login failed: $username disabled (Remote IP: $ip)"); + + return LoginResult::failure($loginData, LoginController::LOGIN_MSG_USERDISABLED); + } + + return $this->processNextOrFinishSuccessfully($loginData); + } + +} diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php index 4e3eea37336..c9aa17b8bc2 100644 --- a/lib/private/User/Manager.php +++ b/lib/private/User/Manager.php @@ -196,7 +196,7 @@ class Manager extends PublicEmitter implements IUserManager { * @internal * @param string $loginName * @param string $password - * @return mixed the User object on success, false otherwise + * @return IUser|false the User object on success, false otherwise */ public function checkPasswordNoLogging($loginName, $password) { $loginName = str_replace("\0", '', $loginName); diff --git a/tests/Core/Controller/LoginControllerTest.php b/tests/Core/Controller/LoginControllerTest.php index bb21903b653..1e7a0d30498 100644 --- a/tests/Core/Controller/LoginControllerTest.php +++ b/tests/Core/Controller/LoginControllerTest.php @@ -21,6 +21,9 @@ namespace Tests\Core\Controller; +use OC\Authentication\Login\Chain as LoginChain; +use OC\Authentication\Login\LoginData; +use OC\Authentication\Login\LoginResult; use OC\Authentication\Token\IToken; use OC\Authentication\TwoFactorAuth\Manager; use OC\Authentication\TwoFactorAuth\ProviderSet; @@ -39,32 +42,47 @@ use OCP\ISession; use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; use Test\TestCase; class LoginControllerTest extends TestCase { + /** @var LoginController */ private $loginController; - /** @var IRequest|\PHPUnit_Framework_MockObject_MockObject */ + + /** @var IRequest|MockObject */ private $request; - /** @var IUserManager|\PHPUnit_Framework_MockObject_MockObject */ + + /** @var IUserManager|MockObject */ private $userManager; - /** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */ + + /** @var IConfig|MockObject */ private $config; - /** @var ISession|\PHPUnit_Framework_MockObject_MockObject */ + + /** @var ISession|MockObject */ private $session; - /** @var Session|\PHPUnit_Framework_MockObject_MockObject */ + + /** @var Session|MockObject */ private $userSession; - /** @var IURLGenerator|\PHPUnit_Framework_MockObject_MockObject */ + + /** @var IURLGenerator|MockObject */ private $urlGenerator; - /** @var ILogger|\PHPUnit_Framework_MockObject_MockObject */ + + /** @var ILogger|MockObject */ private $logger; - /** @var Manager|\PHPUnit_Framework_MockObject_MockObject */ + + /** @var Manager|MockObject */ private $twoFactorManager; - /** @var Defaults|\PHPUnit_Framework_MockObject_MockObject */ + + /** @var Defaults|MockObject */ private $defaults; - /** @var Throttler|\PHPUnit_Framework_MockObject_MockObject */ + + /** @var Throttler|MockObject */ private $throttler; + /** @var LoginChain|MockObject */ + private $chain; + public function setUp() { parent::setUp(); $this->request = $this->createMock(IRequest::class); @@ -77,6 +95,7 @@ class LoginControllerTest extends TestCase { $this->twoFactorManager = $this->createMock(Manager::class); $this->defaults = $this->createMock(Defaults::class); $this->throttler = $this->createMock(Throttler::class); + $this->chain = $this->createMock(LoginChain::class); $this->request->method('getRemoteAddress') ->willReturn('1.2.3.4'); @@ -95,9 +114,9 @@ class LoginControllerTest extends TestCase { $this->userSession, $this->urlGenerator, $this->logger, - $this->twoFactorManager, $this->defaults, - $this->throttler + $this->throttler, + $this->chain ); } @@ -292,51 +311,6 @@ class LoginControllerTest extends TestCase { $this->assertEquals($expectedResponse, $this->loginController->showLoginForm('LdapUser', '', '')); } - /** - * Asserts that a disabled user can't login and gets the expected response. - */ - public function testLoginForDisabledUser() { - /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ - $user = $this->createMock(IUser::class); - $user->method('getUID') - ->willReturn('uid'); - $user->method('isEnabled') - ->willReturn(false); - - $this->request - ->expects($this->once()) - ->method('passesCSRFCheck') - ->willReturn(true); - - $this->userSession - ->method('isLoggedIn') - ->willReturn(false); - - $loginName = 'iMDisabled'; - $password = 'secret'; - - $this->session - ->expects($this->once()) - ->method('set') - ->with('loginMessages', [ - [LoginController::LOGIN_MSG_USERDISABLED], [] - ]); - - $this->userManager - ->expects($this->once()) - ->method('get') - ->with($loginName) - ->willReturn($user); - - $expected = new RedirectResponse(''); - $expected->throttle(['user' => $loginName]); - - $response = $this->loginController->tryLogin( - $loginName, $password, null, false, 'Europe/Berlin', '1' - ); - $this->assertEquals($expected, $response); - } - public function testShowLoginFormForUserNamed0() { $this->userSession ->expects($this->once()) @@ -386,43 +360,34 @@ class LoginControllerTest extends TestCase { ->expects($this->once()) ->method('passesCSRFCheck') ->willReturn(true); - $this->userManager->expects($this->once()) - ->method('checkPasswordNoLogging') - ->will($this->returnValue(false)); - $this->userManager->expects($this->once()) - ->method('getByEmail') - ->with($user) - ->willReturn([]); + $loginData = new LoginData( + $this->request, + $user, + $password, + '/apps/files' + ); + $loginResult = LoginResult::failure($loginData, LoginController::LOGIN_MSG_INVALIDPASSWORD); + $this->chain->expects($this->once()) + ->method('process') + ->with($this->equalTo($loginData)) + ->willReturn($loginResult); $this->urlGenerator->expects($this->once()) ->method('linkToRoute') ->with('core.login.showLoginForm', [ - 'user' => 'MyUserName', + 'user' => $user, 'redirect_url' => '/apps/files', ]) ->will($this->returnValue($loginPageUrl)); - - $this->userSession->expects($this->never()) - ->method('createSessionToken'); - $this->userSession->expects($this->never()) - ->method('createRememberMeToken'); - $this->config->expects($this->never()) - ->method('deleteUserValue'); - $expected = new \OCP\AppFramework\Http\RedirectResponse($loginPageUrl); $expected->throttle(['user' => 'MyUserName']); - $this->assertEquals($expected, $this->loginController->tryLogin($user, $password, '/apps/files')); + + $response = $this->loginController->tryLogin($user, $password, '/apps/files'); + + $this->assertEquals($expected, $response); } public function testLoginWithValidCredentials() { - /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ - $user = $this->createMock(IUser::class); - $user->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('uid')); - $loginName = 'loginli'; - $user->expects($this->any()) - ->method('getLastLogin') - ->willReturn(123456); + $user = 'MyUserName'; $password = 'secret'; $indexPageUrl = \OC_Util::getDefaultPageUrl(); @@ -430,87 +395,25 @@ class LoginControllerTest extends TestCase { ->expects($this->once()) ->method('passesCSRFCheck') ->willReturn(true); - $this->userManager->expects($this->once()) - ->method('checkPasswordNoLogging') - ->will($this->returnValue($user)); - $this->userSession->expects($this->once()) - ->method('completeLogin') - ->with($user, ['loginName' => $loginName, 'password' => $password]); - $this->userSession->expects($this->once()) - ->method('createSessionToken') - ->with($this->request, $user->getUID(), $loginName, $password, IToken::REMEMBER); - $this->twoFactorManager->expects($this->once()) - ->method('isTwoFactorAuthenticated') - ->with($user) - ->will($this->returnValue(false)); - $this->config->expects($this->once()) - ->method('deleteUserValue') - ->with('uid', 'core', 'lostpassword'); - $this->config->expects($this->once()) - ->method('setUserValue') - ->with('uid', 'core', 'timezone', 'Europe/Berlin'); - $this->config - ->method('getSystemValue') - ->with('remember_login_cookie_lifetime') - ->willReturn(1234); - $this->userSession->expects($this->never()) - ->method('createRememberMeToken'); - - $this->session->expects($this->exactly(2)) - ->method('set') - ->withConsecutive( - ['last-password-confirm', 123456], - ['timezone', '1'] - ); - + $loginData = new LoginData( + $this->request, + $user, + $password + ); + $loginResult = LoginResult::success($loginData); + $this->chain->expects($this->once()) + ->method('process') + ->with($this->equalTo($loginData)) + ->willReturn($loginResult); $expected = new \OCP\AppFramework\Http\RedirectResponse($indexPageUrl); - $this->assertEquals($expected, $this->loginController->tryLogin($loginName, $password, null, false, 'Europe/Berlin', '1')); - } - - public function testLoginWithValidCredentialsAndRememberMe() { - /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ - $user = $this->createMock(IUser::class); - $user->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('uid')); - $loginName = 'loginli'; - $password = 'secret'; - $indexPageUrl = \OC_Util::getDefaultPageUrl(); - $this->request - ->expects($this->once()) - ->method('passesCSRFCheck') - ->willReturn(true); - $this->userManager->expects($this->once()) - ->method('checkPasswordNoLogging') - ->will($this->returnValue($user)); - $this->userSession->expects($this->once()) - ->method('completeLogin') - ->with($user, ['loginName' => $loginName, 'password' => $password]); - $this->userSession->expects($this->once()) - ->method('createSessionToken') - ->with($this->request, $user->getUID(), $loginName, $password, true); - $this->twoFactorManager->expects($this->once()) - ->method('isTwoFactorAuthenticated') - ->with($user) - ->will($this->returnValue(false)); - $this->config->expects($this->once()) - ->method('deleteUserValue') - ->with('uid', 'core', 'lostpassword'); - $this->config - ->method('getSystemValue') - ->with('remember_login_cookie_lifetime') - ->willReturn(1234); - $this->userSession->expects($this->once()) - ->method('createRememberMeToken') - ->with($user); + $response = $this->loginController->tryLogin($user, $password); - $expected = new \OCP\AppFramework\Http\RedirectResponse($indexPageUrl); - $this->assertEquals($expected, $this->loginController->tryLogin($loginName, $password, null, true)); + $this->assertEquals($expected, $response); } public function testLoginWithoutPassedCsrfCheckAndNotLoggedIn() { - /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ + /** @var IUser|MockObject $user */ $user = $this->createMock(IUser::class); $user->expects($this->any()) ->method('getUID') @@ -536,7 +439,7 @@ class LoginControllerTest extends TestCase { } public function testLoginWithoutPassedCsrfCheckAndLoggedIn() { - /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ + /** @var IUser|MockObject $user */ $user = $this->createMock(IUser::class); $user->expects($this->any()) ->method('getUID') @@ -571,196 +474,76 @@ class LoginControllerTest extends TestCase { } public function testLoginWithValidCredentialsAndRedirectUrl() { - /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ - $user = $this->createMock(IUser::class); - $user->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('jane')); + $user = 'MyUserName'; $password = 'secret'; - $originalUrl = 'another%20url'; - $redirectUrl = 'http://localhost/another url'; + $indexPageUrl = \OC_Util::getDefaultPageUrl(); + $redirectUrl = 'https://next.cloud/apps/mail'; $this->request ->expects($this->once()) ->method('passesCSRFCheck') ->willReturn(true); - $this->userManager->expects($this->once()) - ->method('checkPasswordNoLogging') - ->with('Jane', $password) - ->will($this->returnValue($user)); - $this->userSession->expects($this->once()) - ->method('createSessionToken') - ->with($this->request, $user->getUID(), 'Jane', $password, IToken::REMEMBER); + $loginData = new LoginData( + $this->request, + $user, + $password, + '%2Fapps%2Fmail' + ); + $loginResult = LoginResult::success($loginData); + $this->chain->expects($this->once()) + ->method('process') + ->with($this->equalTo($loginData)) + ->willReturn($loginResult); $this->userSession->expects($this->once()) ->method('isLoggedIn') - ->with() - ->will($this->returnValue(true)); + ->willReturn(true); $this->urlGenerator->expects($this->once()) ->method('getAbsoluteURL') - ->with(urldecode($originalUrl)) + ->with(urldecode('/apps/mail')) ->will($this->returnValue($redirectUrl)); - $this->config->expects($this->once()) - ->method('deleteUserValue') - ->with('jane', 'core', 'lostpassword'); - $this->config - ->method('getSystemValue') - ->with('remember_login_cookie_lifetime') - ->willReturn(1234); - - $expected = new \OCP\AppFramework\Http\RedirectResponse(urldecode($redirectUrl)); - $this->assertEquals($expected, $this->loginController->tryLogin('Jane', $password, $originalUrl)); - } - - public function testLoginWithOneTwoFactorProvider() { - /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ - $user = $this->createMock(IUser::class); - $user->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('john')); - $password = 'secret'; - $challengeUrl = 'challenge/url'; - $provider1 = $this->createMock(IProvider::class); - $provider1->method('getId')->willReturn('u2f'); - $provider2 = $this->createMock(BackupCodesProvider::class); - $provider2->method('getId')->willReturn('backup'); + $expected = new \OCP\AppFramework\Http\RedirectResponse($redirectUrl); - $this->request - ->expects($this->once()) - ->method('passesCSRFCheck') - ->willReturn(true); - $this->userManager->expects($this->once()) - ->method('checkPasswordNoLogging') - ->will($this->returnValue($user)); - $this->userSession->expects($this->once()) - ->method('completeLogin') - ->with($user, ['loginName' => 'john@doe.com', 'password' => $password]); - $this->userSession->expects($this->once()) - ->method('createSessionToken') - ->with($this->request, $user->getUID(), 'john@doe.com', $password, IToken::REMEMBER); - $this->twoFactorManager->expects($this->once()) - ->method('isTwoFactorAuthenticated') - ->with($user) - ->will($this->returnValue(true)); - $this->twoFactorManager->expects($this->once()) - ->method('prepareTwoFactorLogin') - ->with($user); - $providerSet = new ProviderSet([$provider1, $provider2], false); - $this->twoFactorManager->expects($this->once()) - ->method('getProviderSet') - ->with($user) - ->willReturn($providerSet); - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with('core.TwoFactorChallenge.showChallenge', [ - 'challengeProviderId' => 'u2f', - ]) - ->will($this->returnValue($challengeUrl)); - $this->config->expects($this->once()) - ->method('deleteUserValue') - ->with('john', 'core', 'lostpassword'); - $this->config - ->method('getSystemValue') - ->with('remember_login_cookie_lifetime') - ->willReturn(1234); - $this->userSession->expects($this->never()) - ->method('createRememberMeToken'); + $response = $this->loginController->tryLogin($user, $password, '%2Fapps%2Fmail'); - $expected = new RedirectResponse($challengeUrl); - $this->assertEquals($expected, $this->loginController->tryLogin('john@doe.com', $password, null)); + $this->assertEquals($expected, $response); } - public function testLoginWithMultipleTwoFactorProviders() { - /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ - $user = $this->createMock(IUser::class); - $user->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('john')); - $password = 'secret'; - $challengeUrl = 'challenge/url'; - $provider1 = $this->createMock(IProvider::class); - $provider2 = $this->createMock(IProvider::class); - $provider1->method('getId')->willReturn('prov1'); - $provider2->method('getId')->willReturn('prov2'); - + public function testToNotLeakLoginName() { $this->request ->expects($this->once()) ->method('passesCSRFCheck') ->willReturn(true); - $this->userManager->expects($this->once()) - ->method('checkPasswordNoLogging') - ->will($this->returnValue($user)); - $this->userSession->expects($this->once()) - ->method('completeLogin') - ->with($user, ['loginName' => 'john@doe.com', 'password' => $password]); - $this->userSession->expects($this->once()) - ->method('createSessionToken') - ->with($this->request, $user->getUID(), 'john@doe.com', $password, IToken::REMEMBER); - $this->twoFactorManager->expects($this->once()) - ->method('isTwoFactorAuthenticated') - ->with($user) - ->will($this->returnValue(true)); - $this->twoFactorManager->expects($this->once()) - ->method('prepareTwoFactorLogin') - ->with($user); - $providerSet = new ProviderSet([$provider1, $provider2], false); - $this->twoFactorManager->expects($this->once()) - ->method('getProviderSet') - ->with($user) - ->willReturn($providerSet); + $loginPageUrl = '/login?redirect_url=/apps/files'; + $loginData = new LoginData( + $this->request, + 'john@doe.com', + 'just wrong', + '/apps/files' + ); + $loginResult = LoginResult::failure($loginData, LoginController::LOGIN_MSG_INVALIDPASSWORD); + $this->chain->expects($this->once()) + ->method('process') + ->with($this->equalTo($loginData)) + ->willReturnCallback(function(LoginData $data) use ($loginResult) { + $data->setUsername('john'); + return $loginResult; + }); $this->urlGenerator->expects($this->once()) ->method('linkToRoute') - ->with('core.TwoFactorChallenge.selectChallenge') - ->will($this->returnValue($challengeUrl)); - $this->config->expects($this->once()) - ->method('deleteUserValue') - ->with('john', 'core', 'lostpassword'); - $this->config - ->method('getSystemValue') - ->with('remember_login_cookie_lifetime') - ->willReturn(1234); - $this->userSession->expects($this->never()) - ->method('createRememberMeToken'); - - $expected = new RedirectResponse($challengeUrl); - $this->assertEquals($expected, $this->loginController->tryLogin('john@doe.com', $password, null)); - } - - public function testToNotLeakLoginName() { - /** @var IUser|\PHPUnit_Framework_MockObject_MockObject $user */ - $user = $this->createMock(IUser::class); - $user->expects($this->any()) - ->method('getUID') - ->will($this->returnValue('john')); - - $this->userManager->expects($this->once()) - ->method('checkPasswordNoLogging') - ->with('john@doe.com', 'just wrong') - ->willReturn(false); - $this->userManager->expects($this->once()) - ->method('checkPassword') - ->with('john', 'just wrong') - ->willReturn(false); - - $this->userManager->expects($this->once()) - ->method('getByEmail') - ->with('john@doe.com') - ->willReturn([$user]); + ->with('core.login.showLoginForm', [ + 'user' => 'john@doe.com', + 'redirect_url' => '/apps/files', + ]) + ->will($this->returnValue($loginPageUrl)); + $expected = new \OCP\AppFramework\Http\RedirectResponse($loginPageUrl); + $expected->throttle(['user' => 'john']); - $this->urlGenerator->expects($this->once()) - ->method('linkToRoute') - ->with('core.login.showLoginForm', ['user' => 'john@doe.com']) - ->will($this->returnValue('')); - $this->request - ->expects($this->once()) - ->method('passesCSRFCheck') - ->willReturn(true); - $this->config->expects($this->never()) - ->method('deleteUserValue'); - $this->userSession->expects($this->never()) - ->method('createRememberMeToken'); + $response = $this->loginController->tryLogin( + 'john@doe.com', + 'just wrong', + '/apps/files' + ); - $expected = new RedirectResponse(''); - $expected->throttle(['user' => 'john']); - $this->assertEquals($expected, $this->loginController->tryLogin('john@doe.com', 'just wrong', null)); + $this->assertEquals($expected, $response); } } diff --git a/tests/lib/Authentication/Login/ALoginCommandTest.php b/tests/lib/Authentication/Login/ALoginCommandTest.php new file mode 100644 index 00000000000..ec3b324b2ce --- /dev/null +++ b/tests/lib/Authentication/Login/ALoginCommandTest.php @@ -0,0 +1,122 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace lib\Authentication\Login; + +use OC\Authentication\Login\ALoginCommand; +use OC\Authentication\Login\LoginData; +use OCP\IRequest; +use OCP\IUser; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +abstract class ALoginCommandTest extends TestCase { + + /** @var IRequest|MockObject */ + protected $request; + + /** @var string */ + protected $username = 'user123'; + + /** @var string */ + protected $password = '123456'; + + /** @var string */ + protected $redirectUrl = '/apps/contacts'; + + /** @var string */ + protected $timezone = 'Europe/Vienna'; + + protected $timeZoneOffset = '2'; + + /** @var IUser|MockObject */ + protected $user; + + /** @var ALoginCommand */ + protected $cmd; + + protected function setUp() { + parent::setUp(); + + $this->request = $this->createMock(IRequest::class); + $this->user = $this->createMock(IUser::class); + } + + protected function getBasicLoginData(): LoginData { + return new LoginData( + $this->request, + $this->username, + $this->password + ); + } + + protected function getInvalidLoginData(): LoginData { + return new LoginData( + $this->request, + $this->username, + $this->password + ); + } + + protected function getFailedLoginData(): LoginData { + $data = new LoginData( + $this->request, + $this->username, + $this->password + ); + $data->setUser(false); + return $data; + } + + protected function getLoggedInLoginData(): LoginData { + $basic = $this->getBasicLoginData(); + $basic->setUser($this->user); + return $basic; + } + + protected function getLoggedInLoginDataWithRedirectUrl(): LoginData { + $data = new LoginData( + $this->request, + $this->username, + $this->password, + $this->redirectUrl + ); + $data->setUser($this->user); + return $data; + } + + protected function getLoggedInLoginDataWithTimezone(): LoginData { + $data = new LoginData( + $this->request, + $this->username, + $this->password, + null, + $this->timezone, + $this->timeZoneOffset + ); + $data->setUser($this->user); + return $data; + } + +} diff --git a/tests/lib/Authentication/Login/ClearLostPasswordTokensCommandTest.php b/tests/lib/Authentication/Login/ClearLostPasswordTokensCommandTest.php new file mode 100644 index 00000000000..05078cde930 --- /dev/null +++ b/tests/lib/Authentication/Login/ClearLostPasswordTokensCommandTest.php @@ -0,0 +1,67 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace Test\Authentication\Login; + +use lib\Authentication\Login\ALoginCommandTest; +use OC\Authentication\Login\ClearLostPasswordTokensCommand; +use OC\Authentication\Login\LoginData; +use OCP\IConfig; +use PHPUnit\Framework\MockObject\MockObject; + +class ClearLostPasswordTokensCommandTest extends ALoginCommandTest { + + /** @var IConfig|MockObject */ + private $config; + + protected function setUp() { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + + $this->cmd = new ClearLostPasswordTokensCommand( + $this->config + ); + } + + public function testProcess() { + $data = $this->getLoggedInLoginData(); + $this->user->expects($this->once()) + ->method('getUID') + ->willReturn($this->username); + $this->config->expects($this->once()) + ->method('deleteUserValue') + ->with( + $this->username, + 'core', + 'lostpassword' + ); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + +} diff --git a/tests/lib/Authentication/Login/CompleteLoginCommandTest.php b/tests/lib/Authentication/Login/CompleteLoginCommandTest.php new file mode 100644 index 00000000000..8912dcab09c --- /dev/null +++ b/tests/lib/Authentication/Login/CompleteLoginCommandTest.php @@ -0,0 +1,67 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace lib\Authentication\Login; + +use OC\Authentication\Login\CompleteLoginCommand; +use OC\User\Session; +use PHPUnit\Framework\MockObject\MockObject; + +class CompleteLoginCommandTest extends ALoginCommandTest { + + /** @var Session|MockObject */ + private $session; + + protected function setUp() { + parent::setUp(); + + $this->session = $this->createMock(Session::class); + + $this->cmd = new CompleteLoginCommand( + $this->session + ); + } + + public function testProcess() { + $data = $this->getLoggedInLoginData(); + $this->session->expects($this->once()) + ->method('completeLogin') + ->with( + $this->user, + $this->equalTo( + [ + 'loginName' => $this->username, + 'password' => $this->password, + ] + ) + ); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + + +} \ No newline at end of file diff --git a/tests/lib/Authentication/Login/CreateSessionTokenCommandTest.php b/tests/lib/Authentication/Login/CreateSessionTokenCommandTest.php new file mode 100644 index 00000000000..136906f42b0 --- /dev/null +++ b/tests/lib/Authentication/Login/CreateSessionTokenCommandTest.php @@ -0,0 +1,121 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace lib\Authentication\Login; + +use OC\Authentication\Login\CreateSessionTokenCommand; +use OC\Authentication\Token\IToken; +use OC\User\Session; +use OCP\IConfig; +use PHPUnit\Framework\MockObject\MockObject; + +class CreateSessionTokenCommandTest extends ALoginCommandTest { + + /** @var IConfig|MockObject */ + private $config; + + /** @var Session|MockObject */ + private $userSession; + + protected function setUp() { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + $this->userSession = $this->createMock(Session::class); + + $this->cmd = new CreateSessionTokenCommand( + $this->config, + $this->userSession + ); + } + + public function testProcess() { + $data = $this->getLoggedInLoginData(); + $this->config->expects($this->once()) + ->method('getSystemValue') + ->with( + 'remember_login_cookie_lifetime', + 60 * 60 * 24 * 15 + ) + ->willReturn(100); + $this->user->expects($this->any()) + ->method('getUID') + ->willReturn($this->username); + $this->userSession->expects($this->once()) + ->method('createSessionToken') + ->with( + $this->request, + $this->username, + $this->username, + $this->password, + IToken::REMEMBER + ); + $this->userSession->expects($this->once()) + ->method('updateTokens') + ->with( + $this->username, + $this->username + ); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + + public function testProcessDoNotRemember() { + $data = $this->getLoggedInLoginData(); + $this->config->expects($this->once()) + ->method('getSystemValue') + ->with( + 'remember_login_cookie_lifetime', + 60 * 60 * 24 * 15 + ) + ->willReturn(0); + $this->user->expects($this->any()) + ->method('getUID') + ->willReturn($this->username); + $this->userSession->expects($this->once()) + ->method('createSessionToken') + ->with( + $this->request, + $this->username, + $this->username, + $this->password, + IToken::DO_NOT_REMEMBER + ); + $this->userSession->expects($this->once()) + ->method('updateTokens') + ->with( + $this->username, + $this->username + ); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertFalse($data->isRememberLogin()); + } + +} diff --git a/tests/lib/Authentication/Login/EmailLoginCommandTest.php b/tests/lib/Authentication/Login/EmailLoginCommandTest.php new file mode 100644 index 00000000000..c72048d8877 --- /dev/null +++ b/tests/lib/Authentication/Login/EmailLoginCommandTest.php @@ -0,0 +1,165 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace lib\Authentication\Login; + +use OC\Authentication\Login\EmailLoginCommand; +use OCP\IUser; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; + +class EmailLoginCommandTest extends ALoginCommandTest { + + /** @var IUserManager|MockObject */ + private $userManager; + + protected function setUp() { + parent::setUp(); + + $this->userManager = $this->createMock(IUserManager::class); + + $this->cmd = new EmailLoginCommand( + $this->userManager + ); + } + + public function testProcessAlreadyLoggedIn() { + $data = $this->getLoggedInLoginData(); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + + public function testProcessNotAnEmailLogin() { + $data = $this->getFailedLoginData(); + $this->userManager->expects($this->once()) + ->method('getByEmail') + ->with($this->username) + ->willReturn([]); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + + public function testProcessDuplicateEmailLogin() { + $data = $this->getFailedLoginData(); + $this->userManager->expects($this->once()) + ->method('getByEmail') + ->with($this->username) + ->willReturn([ + $this->createMock(IUser::class), + $this->createMock(IUser::class), + ]); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + + public function testProcessUidIsEmail() { + $email = 'user@domain.com'; + $data = $this->getFailedLoginData(); + $data->setUsername($email); + $emailUser = $this->createMock(IUser::class); + $emailUser->expects($this->any()) + ->method('getUID') + ->willReturn($email); + $this->userManager->expects($this->once()) + ->method('getByEmail') + ->with($email) + ->willReturn([ + $emailUser, + ]); + $this->userManager->expects($this->never()) + ->method('checkPassword'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertFalse($data->getUser()); + $this->assertEquals($email, $data->getUsername()); + } + + public function testProcessWrongPassword() { + $email = 'user@domain.com'; + $data = $this->getFailedLoginData(); + $data->setUsername($email); + $emailUser = $this->createMock(IUser::class); + $emailUser->expects($this->any()) + ->method('getUID') + ->willReturn('user2'); + $this->userManager->expects($this->once()) + ->method('getByEmail') + ->with($email) + ->willReturn([ + $emailUser, + ]); + $this->userManager->expects($this->once()) + ->method('checkPassword') + ->with( + 'user2', + $this->password + ) + ->willReturn(false); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertFalse($data->getUser()); + $this->assertEquals($email, $data->getUsername()); + } + + public function testProcess() { + $email = 'user@domain.com'; + $data = $this->getFailedLoginData(); + $data->setUsername($email); + $emailUser = $this->createMock(IUser::class); + $emailUser->expects($this->any()) + ->method('getUID') + ->willReturn('user2'); + $this->userManager->expects($this->once()) + ->method('getByEmail') + ->with($email) + ->willReturn([ + $emailUser, + ]); + $this->userManager->expects($this->once()) + ->method('checkPassword') + ->with( + 'user2', + $this->password + ) + ->willReturn($emailUser); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertEquals($emailUser, $data->getUser()); + $this->assertEquals('user2', $data->getUsername()); + } + +} diff --git a/tests/lib/Authentication/Login/FinishRememberedLoginCommandTest.php b/tests/lib/Authentication/Login/FinishRememberedLoginCommandTest.php new file mode 100644 index 00000000000..33926aeea0e --- /dev/null +++ b/tests/lib/Authentication/Login/FinishRememberedLoginCommandTest.php @@ -0,0 +1,69 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace lib\Authentication\Login; + +use OC\Authentication\Login\FinishRememberedLoginCommand; +use OC\User\Session; +use PHPUnit\Framework\MockObject\MockObject; + +class FinishRememberedLoginCommandTest extends ALoginCommandTest { + + /** @var Session|MockObject */ + private $userSession; + + protected function setUp() { + parent::setUp(); + + $this->userSession = $this->createMock(Session::class); + + $this->cmd = new FinishRememberedLoginCommand( + $this->userSession + ); + } + + public function testProcessNotRememberedLogin() { + $data = $this->getLoggedInLoginData(); + $data->setRememberLogin(false); + $this->userSession->expects($this->never()) + ->method('createRememberMeToken'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + + public function testProcess() { + $data = $this->getLoggedInLoginData(); + $this->userSession->expects($this->once()) + ->method('createRememberMeToken') + ->with($this->user); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + +} diff --git a/tests/lib/Authentication/Login/LoggedInCheckCommandTest.php b/tests/lib/Authentication/Login/LoggedInCheckCommandTest.php new file mode 100644 index 00000000000..16938307338 --- /dev/null +++ b/tests/lib/Authentication/Login/LoggedInCheckCommandTest.php @@ -0,0 +1,67 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace lib\Authentication\Login; + +use OC\Authentication\Login\LoggedInCheckCommand; +use OC\Core\Controller\LoginController; +use OCP\ILogger; +use PHPUnit\Framework\MockObject\MockObject; + +class LoggedInCheckCommandTest extends ALoginCommandTest { + + /** @var ILogger|MockObject */ + private $logger; + + protected function setUp() { + parent::setUp(); + + $this->logger = $this->createMock(ILogger::class); + + $this->cmd = new LoggedInCheckCommand( + $this->logger + ); + } + + public function testProcessSuccessfulLogin() { + $data = $this->getLoggedInLoginData(); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + + public function testProcessFailedLogin() { + $data = $this->getFailedLoginData(); + $this->logger->expects($this->once()) + ->method('warning'); + + $result = $this->cmd->process($data); + + $this->assertFalse($result->isSuccess()); + $this->assertSame(LoginController::LOGIN_MSG_INVALIDPASSWORD, $result->getErrorMessage()); + } + +} diff --git a/tests/lib/Authentication/Login/PreLoginHookCommandTest.php b/tests/lib/Authentication/Login/PreLoginHookCommandTest.php new file mode 100644 index 00000000000..3329f3ad82f --- /dev/null +++ b/tests/lib/Authentication/Login/PreLoginHookCommandTest.php @@ -0,0 +1,66 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace lib\Authentication\Login; + +use OC\Authentication\Login\PreLoginHookCommand; +use OC\User\Manager; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; + +class PreLoginHookCommandTest extends ALoginCommandTest { + + /** @var IUserManager|MockObject */ + private $userManager; + + protected function setUp() { + parent::setUp(); + + $this->userManager = $this->createMock(Manager::class); + + $this->cmd = new PreLoginHookCommand( + $this->userManager + ); + } + + public function testProcess() { + $data = $this->getBasicLoginData(); + $this->userManager->expects($this->once()) + ->method('emit') + ->with( + '\OC\User', + 'preLogin', + [ + $this->username, + $this->password, + ] + ); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + +} diff --git a/tests/lib/Authentication/Login/SetUserTimezoneCommandTest.php b/tests/lib/Authentication/Login/SetUserTimezoneCommandTest.php new file mode 100644 index 00000000000..a2ce656a14b --- /dev/null +++ b/tests/lib/Authentication/Login/SetUserTimezoneCommandTest.php @@ -0,0 +1,90 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace lib\Authentication\Login; + +use OC\Authentication\Login\SetUserTimezoneCommand; +use OCP\IConfig; +use OCP\ISession; +use PHPUnit\Framework\MockObject\MockObject; + +class SetUserTimezoneCommandTest extends ALoginCommandTest { + + /** @var IConfig|MockObject */ + private $config; + + /** @var ISession|MockObject */ + private $session; + + protected function setUp() { + parent::setUp(); + + $this->config = $this->createMock(IConfig::class); + $this->session = $this->createMock(ISession::class); + + $this->cmd = new SetUserTimezoneCommand( + $this->config, + $this->session + ); + } + + public function testProcessNoTimezoneSet() { + $data = $this->getLoggedInLoginData(); + $this->config->expects($this->never()) + ->method('setUserValue'); + $this->session->expects($this->never()) + ->method('set'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + + public function testProcess() { + $data = $this->getLoggedInLoginDataWithTimezone(); + $this->user->expects($this->once()) + ->method('getUID') + ->willReturn($this->username); + $this->config->expects($this->once()) + ->method('setUserValue') + ->with( + $this->username, + 'core', + 'timezone', + $this->timezone + ); + $this->session->expects($this->once()) + ->method('set') + ->with( + 'timezone', + $this->timeZoneOffset + ); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + +} diff --git a/tests/lib/Authentication/Login/TwoFactorCommandTest.php b/tests/lib/Authentication/Login/TwoFactorCommandTest.php new file mode 100644 index 00000000000..a5c1c8e352b --- /dev/null +++ b/tests/lib/Authentication/Login/TwoFactorCommandTest.php @@ -0,0 +1,179 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace lib\Authentication\Login; + +use OC\Authentication\Login\TwoFactorCommand; +use OC\Authentication\TwoFactorAuth\Manager; +use OC\Authentication\TwoFactorAuth\ProviderSet; +use OCP\Authentication\TwoFactorAuth\IProvider as ITwoFactorAuthProvider; +use OCP\IURLGenerator; +use PHPUnit\Framework\MockObject\MockObject; + +class TwoFactorCommandTest extends ALoginCommandTest { + + /** @var Manager|MockObject */ + private $twoFactorManager; + + /** @var IURLGenerator|MockObject */ + private $urlGenerator; + + protected function setUp() { + parent::setUp(); + + $this->twoFactorManager = $this->createMock(Manager::class); + $this->urlGenerator = $this->createMock(IURLGenerator::class); + + $this->cmd = new TwoFactorCommand( + $this->twoFactorManager, + $this->urlGenerator + ); + } + + public function testNotTwoFactorAuthenticated() { + $data = $this->getLoggedInLoginData(); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->willReturn(false); + $this->twoFactorManager->expects($this->never()) + ->method('prepareTwoFactorLogin'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + + public function testProcessOneActiveProvider() { + $data = $this->getLoggedInLoginData(); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->willReturn(true); + $this->twoFactorManager->expects($this->once()) + ->method('prepareTwoFactorLogin') + ->with( + $this->user, + $data->isRememberLogin() + ); + $provider = $this->createMock(ITwoFactorAuthProvider::class); + $this->twoFactorManager->expects($this->once()) + ->method('getProviderSet') + ->willReturn(new ProviderSet([ + $provider, + ], false)); + $provider->expects($this->once()) + ->method('getId') + ->willReturn('test'); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with( + 'core.TwoFactorChallenge.showChallenge', + [ + 'challengeProviderId' => 'test' + ] + ) + ->willReturn('two/factor/url'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertEquals('two/factor/url', $result->getRedirectUrl()); + } + + public function testProcessTwoActiveProviders() { + $data = $this->getLoggedInLoginData(); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->willReturn(true); + $this->twoFactorManager->expects($this->once()) + ->method('prepareTwoFactorLogin') + ->with( + $this->user, + $data->isRememberLogin() + ); + $provider1 = $this->createMock(ITwoFactorAuthProvider::class); + $provider2 = $this->createMock(ITwoFactorAuthProvider::class); + $provider1->expects($this->once()) + ->method('getId') + ->willReturn('test1'); + $provider2->expects($this->once()) + ->method('getId') + ->willReturn('test2'); + $this->twoFactorManager->expects($this->once()) + ->method('getProviderSet') + ->willReturn(new ProviderSet([ + $provider1, + $provider2, + ], false)); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with( + 'core.TwoFactorChallenge.selectChallenge' + ) + ->willReturn('two/factor/url'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertEquals('two/factor/url', $result->getRedirectUrl()); + } + + public function testProcessWithRedirectUrl() { + $data = $this->getLoggedInLoginDataWithRedirectUrl(); + $this->twoFactorManager->expects($this->once()) + ->method('isTwoFactorAuthenticated') + ->willReturn(true); + $this->twoFactorManager->expects($this->once()) + ->method('prepareTwoFactorLogin') + ->with( + $this->user, + $data->isRememberLogin() + ); + $provider = $this->createMock(ITwoFactorAuthProvider::class); + $this->twoFactorManager->expects($this->once()) + ->method('getProviderSet') + ->willReturn(new ProviderSet([ + $provider, + ], false)); + $provider->expects($this->once()) + ->method('getId') + ->willReturn('test'); + $this->urlGenerator->expects($this->once()) + ->method('linkToRoute') + ->with( + 'core.TwoFactorChallenge.showChallenge', + [ + 'challengeProviderId' => 'test', + 'redirect_url' => $this->redirectUrl, + ] + ) + ->willReturn('two/factor/url'); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertEquals('two/factor/url', $result->getRedirectUrl()); + } + +} diff --git a/tests/lib/Authentication/Login/UidLoginCommandTest.php b/tests/lib/Authentication/Login/UidLoginCommandTest.php new file mode 100644 index 00000000000..0a7889c227c --- /dev/null +++ b/tests/lib/Authentication/Login/UidLoginCommandTest.php @@ -0,0 +1,80 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace lib\Authentication\Login; + +use OC\Authentication\Login\UidCheckCommand; +use OC\Authentication\Login\UidLoginCommand; +use OC\User\Manager; +use PHPUnit\Framework\MockObject\MockObject; + +class UidLoginCommandTest extends ALoginCommandTest { + + /** @var Manager|MockObject */ + private $userManager; + + protected function setUp() { + parent::setUp(); + + $this->userManager = $this->createMock(Manager::class); + + $this->cmd = new UidLoginCommand( + $this->userManager + ); + } + + public function testProcessFailingLogin() { + $data = $this->getBasicLoginData(); + $this->userManager->expects($this->once()) + ->method('checkPasswordNoLogging') + ->with( + $this->username, + $this->password + ) + ->willReturn(false); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertFalse($data->getUser()); + } + + public function testProcess() { + $data = $this->getBasicLoginData(); + $this->userManager->expects($this->once()) + ->method('checkPasswordNoLogging') + ->with( + $this->username, + $this->password + ) + ->willReturn($this->user); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + $this->assertEquals($this->user, $data->getUser()); + } + +} diff --git a/tests/lib/Authentication/Login/UpdateLastPasswordConfirmCommandTest.php b/tests/lib/Authentication/Login/UpdateLastPasswordConfirmCommandTest.php new file mode 100644 index 00000000000..5f5bd3032a8 --- /dev/null +++ b/tests/lib/Authentication/Login/UpdateLastPasswordConfirmCommandTest.php @@ -0,0 +1,64 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace lib\Authentication\Login; + +use OC\Authentication\Login\UpdateLastPasswordConfirmCommand; +use OCP\ISession; +use PHPUnit\Framework\MockObject\MockObject; + +class UpdateLastPasswordConfirmCommandTest extends ALoginCommandTest { + + /** @var ISession|MockObject */ + private $session; + + protected function setUp() { + parent::setUp(); + + $this->session = $this->createMock(ISession::class); + + $this->cmd = new UpdateLastPasswordConfirmCommand( + $this->session + ); + } + + public function testProcess() { + $data = $this->getLoggedInLoginData(); + $this->user->expects($this->once()) + ->method('getLastLogin') + ->willReturn(1234); + $this->session->expects($this->once()) + ->method('set') + ->with( + 'last-password-confirm', + 1234 + ); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + +} diff --git a/tests/lib/Authentication/Login/UserDisabledCheckCommandTest.php b/tests/lib/Authentication/Login/UserDisabledCheckCommandTest.php new file mode 100644 index 00000000000..35d8ac63de6 --- /dev/null +++ b/tests/lib/Authentication/Login/UserDisabledCheckCommandTest.php @@ -0,0 +1,97 @@ + + * + * @author 2019 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +declare(strict_types=1); + +namespace lib\Authentication\Login; + +use OC\Authentication\Login\UserDisabledCheckCommand; +use OC\Core\Controller\LoginController; +use OCP\ILogger; +use OCP\IUserManager; +use PHPUnit\Framework\MockObject\MockObject; + +class UserDisabledCheckCommandTest extends ALoginCommandTest { + + /** @var IUserManager|MockObject */ + private $userManager; + + /** @var ILogger|MockObject */ + private $logger; + + protected function setUp() { + parent::setUp(); + + $this->userManager = $this->createMock(IUserManager::class); + $this->logger = $this->createMock(ILogger::class); + + $this->cmd = new UserDisabledCheckCommand( + $this->userManager, + $this->logger + ); + } + + public function testProcessNonExistingUser() { + $data = $this->getBasicLoginData(); + $this->userManager->expects($this->once()) + ->method('get') + ->with($this->username) + ->willReturn(null); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + + public function testProcessDisabledUser() { + $data = $this->getBasicLoginData(); + $this->userManager->expects($this->once()) + ->method('get') + ->with($this->username) + ->willReturn($this->user); + $this->user->expects($this->once()) + ->method('isEnabled') + ->willReturn(false); + + $result = $this->cmd->process($data); + + $this->assertFalse($result->isSuccess()); + $this->assertSame(LoginController::LOGIN_MSG_USERDISABLED, $result->getErrorMessage()); + } + + public function testProcess() { + $data = $this->getBasicLoginData(); + $this->userManager->expects($this->once()) + ->method('get') + ->with($this->username) + ->willReturn($this->user); + $this->user->expects($this->once()) + ->method('isEnabled') + ->willReturn(true); + + $result = $this->cmd->process($data); + + $this->assertTrue($result->isSuccess()); + } + +} -- 2.39.5