]> source.dussan.org Git - nextcloud-server.git/commitdiff
Add a login chain to reduce the complexity of LoginController::tryLogin 15365/head
authorChristoph Wurst <christoph@winzerhof-wurst.at>
Fri, 3 May 2019 13:09:19 +0000 (15:09 +0200)
committerChristoph Wurst <christoph@winzerhof-wurst.at>
Tue, 7 May 2019 16:04:36 +0000 (18:04 +0200)
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
34 files changed:
core/Controller/LoginController.php
lib/composer/composer/autoload_classmap.php
lib/composer/composer/autoload_static.php
lib/private/Authentication/Login/ALoginCommand.php [new file with mode: 0644]
lib/private/Authentication/Login/Chain.php [new file with mode: 0644]
lib/private/Authentication/Login/ClearLostPasswordTokensCommand.php [new file with mode: 0644]
lib/private/Authentication/Login/CompleteLoginCommand.php [new file with mode: 0644]
lib/private/Authentication/Login/CreateSessionTokenCommand.php [new file with mode: 0644]
lib/private/Authentication/Login/EmailLoginCommand.php [new file with mode: 0644]
lib/private/Authentication/Login/FinishRememberedLoginCommand.php [new file with mode: 0644]
lib/private/Authentication/Login/LoggedInCheckCommand.php [new file with mode: 0644]
lib/private/Authentication/Login/LoginData.php [new file with mode: 0644]
lib/private/Authentication/Login/LoginResult.php [new file with mode: 0644]
lib/private/Authentication/Login/PreLoginHookCommand.php [new file with mode: 0644]
lib/private/Authentication/Login/SetUserTimezoneCommand.php [new file with mode: 0644]
lib/private/Authentication/Login/TwoFactorCommand.php [new file with mode: 0644]
lib/private/Authentication/Login/UidLoginCommand.php [new file with mode: 0644]
lib/private/Authentication/Login/UpdateLastPasswordConfirmCommand.php [new file with mode: 0644]
lib/private/Authentication/Login/UserDisabledCheckCommand.php [new file with mode: 0644]
lib/private/User/Manager.php
tests/Core/Controller/LoginControllerTest.php
tests/lib/Authentication/Login/ALoginCommandTest.php [new file with mode: 0644]
tests/lib/Authentication/Login/ClearLostPasswordTokensCommandTest.php [new file with mode: 0644]
tests/lib/Authentication/Login/CompleteLoginCommandTest.php [new file with mode: 0644]
tests/lib/Authentication/Login/CreateSessionTokenCommandTest.php [new file with mode: 0644]
tests/lib/Authentication/Login/EmailLoginCommandTest.php [new file with mode: 0644]
tests/lib/Authentication/Login/FinishRememberedLoginCommandTest.php [new file with mode: 0644]
tests/lib/Authentication/Login/LoggedInCheckCommandTest.php [new file with mode: 0644]
tests/lib/Authentication/Login/PreLoginHookCommandTest.php [new file with mode: 0644]
tests/lib/Authentication/Login/SetUserTimezoneCommandTest.php [new file with mode: 0644]
tests/lib/Authentication/Login/TwoFactorCommandTest.php [new file with mode: 0644]
tests/lib/Authentication/Login/UidLoginCommandTest.php [new file with mode: 0644]
tests/lib/Authentication/Login/UpdateLastPasswordConfirmCommandTest.php [new file with mode: 0644]
tests/lib/Authentication/Login/UserDisabledCheckCommandTest.php [new file with mode: 0644]

index 85d3b6b837fdc8301f75d40222b1a7dbdab167a6..9c11a37639aa91e7d95b7bad8cd6f61842d39258 100644 (file)
@@ -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(
index cad7c21a5943738882375864203a760cc5bc5211..e47cfe26b72be4ad4f421cf9a12d204e27514cd8 100644 (file)
@@ -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',
index 7a49d254e904cc253ae130abcea9cb9342031686..d5d783aa114330d01c33ef6f453371a71e3a6883 100644 (file)
@@ -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 (file)
index 0000000..368cdaf
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..c8e8293
--- /dev/null
@@ -0,0 +1,111 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..77fddd5
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..84f96c2
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..14ad6d1
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..90d226d
--- /dev/null
@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..84f387a
--- /dev/null
@@ -0,0 +1,46 @@
+<?php
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..3f70385
--- /dev/null
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..a1e8ff1
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..e2eed34
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..87c52c6
--- /dev/null
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..0c74127
--- /dev/null
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..2825dc1
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..3451284
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..b4ae968
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..a37244e
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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);
+       }
+
+}
index 4e3eea37336207977a32543578e70839bd32e687..c9aa17b8bc2a2be181f09d7d7642f7557a360219 100644 (file)
@@ -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);
index bb21903b65350903bef572c0917c206e8232347a..1e7a0d30498f53553f6941f7649484b38dec9f51 100644 (file)
@@ -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 (file)
index 0000000..ec3b324
--- /dev/null
@@ -0,0 +1,122 @@
+<?php
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..05078cd
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..8912dca
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..136906f
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..c72048d
--- /dev/null
@@ -0,0 +1,165 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..33926ae
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..1693830
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..3329f3a
--- /dev/null
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..a2ce656
--- /dev/null
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..a5c1c8e
--- /dev/null
@@ -0,0 +1,179 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..0a7889c
--- /dev/null
@@ -0,0 +1,80 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..5f5bd30
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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 (file)
index 0000000..35d8ac6
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+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());
+       }
+
+}