diff options
author | Lukas Reschke <lukas@statuscode.ch> | 2016-07-21 00:31:02 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-07-21 00:31:02 +0200 |
commit | c385423d1096c243050fed3585734c308115864b (patch) | |
tree | 1002bfc475cd88a7cc495f4ffc23bbd03ec75d39 | |
parent | 020a2a6958e48f7a3a29daa2235f6729980850af (diff) | |
parent | c1589f163c44839fba9b2d3dcfb1e45ee7fa47ef (diff) | |
download | nextcloud-server-c385423d1096c243050fed3585734c308115864b.tar.gz nextcloud-server-c385423d1096c243050fed3585734c308115864b.zip |
Merge pull request #479 from nextcloud/add-bruteforce-throttler
Implement brute force protection
25 files changed, 701 insertions, 68 deletions
diff --git a/apps/dav/appinfo/v1/caldav.php b/apps/dav/appinfo/v1/caldav.php index 50348a60202..975fd34ae8e 100644 --- a/apps/dav/appinfo/v1/caldav.php +++ b/apps/dav/appinfo/v1/caldav.php @@ -35,6 +35,7 @@ $authBackend = new Auth( \OC::$server->getUserSession(), \OC::$server->getRequest(), \OC::$server->getTwoFactorAuthManager(), + \OC::$server->getBruteForceThrottler(), 'principals/' ); $principalBackend = new Principal( diff --git a/apps/dav/appinfo/v1/carddav.php b/apps/dav/appinfo/v1/carddav.php index fc7aff4a63c..e2d8944fcb6 100644 --- a/apps/dav/appinfo/v1/carddav.php +++ b/apps/dav/appinfo/v1/carddav.php @@ -36,6 +36,7 @@ $authBackend = new Auth( \OC::$server->getUserSession(), \OC::$server->getRequest(), \OC::$server->getTwoFactorAuthManager(), + \OC::$server->getBruteForceThrottler(), 'principals/' ); $principalBackend = new Principal( diff --git a/apps/dav/appinfo/v1/webdav.php b/apps/dav/appinfo/v1/webdav.php index 3b733c0fbd5..2af49177ce1 100644 --- a/apps/dav/appinfo/v1/webdav.php +++ b/apps/dav/appinfo/v1/webdav.php @@ -43,6 +43,7 @@ $authBackend = new \OCA\DAV\Connector\Sabre\Auth( \OC::$server->getUserSession(), \OC::$server->getRequest(), \OC::$server->getTwoFactorAuthManager(), + \OC::$server->getBruteForceThrottler(), 'principals/' ); $requestUri = \OC::$server->getRequest()->getRequestUri(); diff --git a/apps/dav/lib/Connector/Sabre/Auth.php b/apps/dav/lib/Connector/Sabre/Auth.php index 28e4ae2bcde..3f9e16b04c5 100644 --- a/apps/dav/lib/Connector/Sabre/Auth.php +++ b/apps/dav/lib/Connector/Sabre/Auth.php @@ -33,6 +33,7 @@ use Exception; use OC\AppFramework\Http\Request; use OC\Authentication\Exceptions\PasswordLoginForbiddenException; use OC\Authentication\TwoFactorAuth\Manager; +use OC\Security\Bruteforce\Throttler; use OC\User\Session; use OCA\DAV\Connector\Sabre\Exception\PasswordLoginForbidden; use OCP\IRequest; @@ -58,23 +59,28 @@ class Auth extends AbstractBasic { private $currentUser; /** @var Manager */ private $twoFactorManager; + /** @var Throttler */ + private $throttler; /** * @param ISession $session * @param Session $userSession * @param IRequest $request * @param Manager $twoFactorManager + * @param Throttler $throttler * @param string $principalPrefix */ public function __construct(ISession $session, Session $userSession, IRequest $request, Manager $twoFactorManager, + Throttler $throttler, $principalPrefix = 'principals/users/') { $this->session = $session; $this->userSession = $userSession; $this->twoFactorManager = $twoFactorManager; $this->request = $request; + $this->throttler = $throttler; $this->principalPrefix = $principalPrefix; // setup realm @@ -107,6 +113,7 @@ class Auth extends AbstractBasic { * @param string $username * @param string $password * @return bool + * @throws PasswordLoginForbidden */ protected function validateUserPass($username, $password) { if ($this->userSession->isLoggedIn() && @@ -118,7 +125,7 @@ class Auth extends AbstractBasic { } else { \OC_Util::setupFS(); //login hooks may need early access to the filesystem try { - if ($this->userSession->logClientIn($username, $password, $this->request)) { + if ($this->userSession->logClientIn($username, $password, $this->request, $this->throttler)) { \OC_Util::setupFS($this->userSession->getUser()->getUID()); $this->session->set(self::DAV_AUTHENTICATED, $this->userSession->getUser()->getUID()); $this->session->close(); diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index 73a07072d3c..982d299c951 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -64,7 +64,8 @@ class Server { \OC::$server->getSession(), \OC::$server->getUserSession(), \OC::$server->getRequest(), - \OC::$server->getTwoFactorAuthManager() + \OC::$server->getTwoFactorAuthManager(), + \OC::$server->getBruteForceThrottler() ); // Set URL explicitly due to reverse-proxy situations diff --git a/apps/dav/tests/unit/Connector/Sabre/AuthTest.php b/apps/dav/tests/unit/Connector/Sabre/AuthTest.php index 92798797d6c..142b83a45b8 100644 --- a/apps/dav/tests/unit/Connector/Sabre/AuthTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/AuthTest.php @@ -28,6 +28,7 @@ namespace OCA\DAV\Tests\unit\Connector\Sabre; use OC\Authentication\TwoFactorAuth\Manager; +use OC\Security\Bruteforce\Throttler; use OC\User\Session; use OCP\IRequest; use OCP\ISession; @@ -51,6 +52,8 @@ class AuthTest extends TestCase { private $request; /** @var Manager */ private $twoFactorManager; + /** @var Throttler */ + private $throttler; public function setUp() { parent::setUp(); @@ -63,11 +66,15 @@ class AuthTest extends TestCase { $this->twoFactorManager = $this->getMockBuilder('\OC\Authentication\TwoFactorAuth\Manager') ->disableOriginalConstructor() ->getMock(); + $this->throttler = $this->getMockBuilder('\OC\Security\Bruteforce\Throttler') + ->disableOriginalConstructor() + ->getMock(); $this->auth = new \OCA\DAV\Connector\Sabre\Auth( $this->session, $this->userSession, $this->request, - $this->twoFactorManager + $this->twoFactorManager, + $this->throttler ); } diff --git a/build/integration/run.sh b/build/integration/run.sh index 2abceaa1fad..eccb378eec4 100755 --- a/build/integration/run.sh +++ b/build/integration/run.sh @@ -9,6 +9,9 @@ else exit 1 fi +# Disable bruteforce protection because the integration tests do trigger them +../../occ config:system:set auth.bruteforce.protection.enabled --value false --type bool + composer install SCENARIO_TO_RUN=$1 diff --git a/config/config.sample.php b/config/config.sample.php index 051e5422fe5..c9f5fecf5f9 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -208,6 +208,13 @@ $CONFIG = array( 'token_auth_enforced' => false, /** + * Whether the bruteforce protection shipped with Nextcloud should be enabled or not. + * + * Disabling this is discouraged for security reasons. + */ +'auth.bruteforce.protection.enabled' => true, + +/** * The directory where the skeleton files are located. These files will be * copied to the data directory of new users. Leave empty to not copy any * skeleton files. diff --git a/core/Application.php b/core/Application.php index 1485f7a7516..82ec5ad023c 100644 --- a/core/Application.php +++ b/core/Application.php @@ -103,7 +103,8 @@ class Application extends App { $c->query('Session'), $c->query('UserSession'), $c->query('URLGenerator'), - $c->query('TwoFactorAuthManager') + $c->query('TwoFactorAuthManager'), + $c->query('ServerContainer')->getBruteforceThrottler() ); }); $container->registerService('TwoFactorChallengeController', function (SimpleContainer $c) { diff --git a/core/Controller/LoginController.php b/core/Controller/LoginController.php index 7806e1de904..66bb13dbb54 100644 --- a/core/Controller/LoginController.php +++ b/core/Controller/LoginController.php @@ -22,7 +22,9 @@ namespace OC\Core\Controller; +use OC\AppFramework\Utility\TimeFactory; use OC\Authentication\TwoFactorAuth\Manager; +use OC\Security\Bruteforce\Throttler; use OC\User\Session; use OC_App; use OC_Util; @@ -37,24 +39,20 @@ use OCP\IUser; use OCP\IUserManager; class LoginController extends Controller { - /** @var IUserManager */ private $userManager; - /** @var IConfig */ private $config; - /** @var ISession */ private $session; - /** @var Session */ private $userSession; - /** @var IURLGenerator */ private $urlGenerator; - /** @var Manager */ private $twoFactorManager; + /** @var Throttler */ + private $throttler; /** * @param string $appName @@ -65,9 +63,17 @@ class LoginController extends Controller { * @param Session $userSession * @param IURLGenerator $urlGenerator * @param Manager $twoFactorManager + * @param Throttler $throttler */ - function __construct($appName, IRequest $request, IUserManager $userManager, IConfig $config, ISession $session, - Session $userSession, IURLGenerator $urlGenerator, Manager $twoFactorManager) { + function __construct($appName, + IRequest $request, + IUserManager $userManager, + IConfig $config, + ISession $session, + Session $userSession, + IURLGenerator $urlGenerator, + Manager $twoFactorManager, + Throttler $throttler) { parent::__construct($appName, $request); $this->userManager = $userManager; $this->config = $config; @@ -75,6 +81,7 @@ class LoginController extends Controller { $this->userSession = $userSession; $this->urlGenerator = $urlGenerator; $this->twoFactorManager = $twoFactorManager; + $this->throttler = $throttler; } /** @@ -171,6 +178,9 @@ class LoginController extends Controller { * @return RedirectResponse */ public function tryLogin($user, $password, $redirect_url) { + $currentDelay = $this->throttler->getDelay($this->request->getRemoteAddress()); + $this->throttler->sleepDelay($this->request->getRemoteAddress()); + $originalUser = $user; // TODO: Add all the insane error handling /* @var $loginResult IUser */ @@ -184,6 +194,10 @@ class LoginController extends Controller { } } if ($loginResult === false) { + $this->throttler->registerAttempt('login', $this->request->getRemoteAddress(), ['user' => $originalUser]); + if($currentDelay === 0) { + $this->throttler->sleepDelay($this->request->getRemoteAddress()); + } $this->session->set('loginMessages', [ ['invalidpassword'] ]); diff --git a/core/Controller/TokenController.php b/core/Controller/TokenController.php index 13b1db9044a..8401c4f23a9 100644 --- a/core/Controller/TokenController.php +++ b/core/Controller/TokenController.php @@ -1,5 +1,4 @@ <?php - /** * @author Christoph Wurst <christoph@owncloud.com> * @@ -23,6 +22,7 @@ namespace OC\Core\Controller; use OC\AppFramework\Http; +use OC\AppFramework\Utility\TimeFactory; use OC\Authentication\Token\DefaultTokenProvider; use OC\Authentication\Token\IProvider; use OC\Authentication\Token\IToken; @@ -35,27 +35,29 @@ use OCP\IRequest; use OCP\Security\ISecureRandom; class TokenController extends Controller { - /** @var UserManager */ private $userManager; - /** @var IProvider */ private $tokenProvider; - /** @var TwoFactorAuthManager */ private $twoFactorAuthManager; - /** @var ISecureRandom */ private $secureRandom; /** * @param string $appName * @param IRequest $request - * @param Manager $userManager - * @param DefaultTokenProvider $tokenProvider + * @param UserManager $userManager + * @param IProvider $tokenProvider + * @param TwoFactorAuthManager $twoFactorAuthManager * @param ISecureRandom $secureRandom */ - public function __construct($appName, IRequest $request, UserManager $userManager, IProvider $tokenProvider, TwoFactorAuthManager $twoFactorAuthManager, ISecureRandom $secureRandom) { + public function __construct($appName, + IRequest $request, + UserManager $userManager, + IProvider $tokenProvider, + TwoFactorAuthManager $twoFactorAuthManager, + ISecureRandom $secureRandom) { parent::__construct($appName, $request); $this->userManager = $userManager; $this->tokenProvider = $tokenProvider; diff --git a/db_structure.xml b/db_structure.xml index 1127f0d82d4..04c91ea494f 100644 --- a/db_structure.xml +++ b/db_structure.xml @@ -1166,6 +1166,84 @@ <table> <!-- + List of usernames, their display name and login password. + --> + <name>*dbprefix*bruteforce_attempts</name> + + <declaration> + <field> + <name>id</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <autoincrement>1</autoincrement> + <unsigned>true</unsigned> + <length>4</length> + </field> + + <field> + <name>action</name> + <type>text</type> + <default></default> + <notnull>true</notnull> + <length>64</length> + </field> + + <field> + <name>occurred</name> + <type>integer</type> + <default>0</default> + <notnull>true</notnull> + <unsigned>true</unsigned> + <length>4</length> + </field> + + <field> + <name>ip</name> + <type>text</type> + <default></default> + <notnull>true</notnull> + <length>255</length> + </field> + + <field> + <name>subnet</name> + <type>text</type> + <default></default> + <notnull>true</notnull> + <length>255</length> + </field> + + <field> + <name>metadata</name> + <type>text</type> + <default></default> + <notnull>true</notnull> + <length>255</length> + </field> + + <index> + <name>bruteforce_attempts_ip</name> + <field> + <name>ip</name> + <sorting>ascending</sorting> + </field> + </index> + <index> + <name>bruteforce_attempts_subnet</name> + <field> + <name>subnet</name> + <sorting>ascending</sorting> + </field> + </index> + + </declaration> + + </table> + + <table> + + <!-- List of tags (category) + a unique tag id (id) per user (uid) and type. --> <name>*dbprefix*vcategory</name> diff --git a/lib/base.php b/lib/base.php index afa13cd1047..b2ac0bab7d8 100644 --- a/lib/base.php +++ b/lib/base.php @@ -993,7 +993,7 @@ class OC { if ($userSession->tryTokenLogin($request)) { return true; } - if ($userSession->tryBasicAuthLogin($request)) { + if ($userSession->tryBasicAuthLogin($request, \OC::$server->getBruteForceThrottler())) { return true; } return false; diff --git a/lib/private/AppFramework/DependencyInjection/DIContainer.php b/lib/private/AppFramework/DependencyInjection/DIContainer.php index 32a85606abf..893d6cb9aa6 100644 --- a/lib/private/AppFramework/DependencyInjection/DIContainer.php +++ b/lib/private/AppFramework/DependencyInjection/DIContainer.php @@ -352,7 +352,8 @@ class DIContainer extends SimpleContainer implements IAppContainer { return new CORSMiddleware( $c['Request'], $c['ControllerMethodReflector'], - $c['OCP\IUserSession'] + $c['OCP\IUserSession'], + $c->getServer()->getBruteForceThrottler() ); }); diff --git a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php index 32a507623e3..04de4bc92d3 100644 --- a/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php +++ b/lib/private/AppFramework/Middleware/Security/CORSMiddleware.php @@ -27,6 +27,7 @@ namespace OC\AppFramework\Middleware\Security; use OC\AppFramework\Middleware\Security\Exceptions\SecurityException; use OC\AppFramework\Utility\ControllerMethodReflector; use OC\Authentication\Exceptions\PasswordLoginForbiddenException; +use OC\Security\Bruteforce\Throttler; use OC\User\Session; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; @@ -42,33 +43,29 @@ use OCP\IRequest; * https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS */ class CORSMiddleware extends Middleware { - - /** - * @var IRequest - */ + /** @var IRequest */ private $request; - - /** - * @var ControllerMethodReflector - */ + /** @var ControllerMethodReflector */ private $reflector; - - /** - * @var Session - */ + /** @var Session */ private $session; + /** @var Throttler */ + private $throttler; /** * @param IRequest $request * @param ControllerMethodReflector $reflector * @param Session $session + * @param Throttler $throttler */ public function __construct(IRequest $request, ControllerMethodReflector $reflector, - Session $session) { + Session $session, + Throttler $throttler) { $this->request = $request; $this->reflector = $reflector; $this->session = $session; + $this->throttler = $throttler; } /** @@ -91,7 +88,7 @@ class CORSMiddleware extends Middleware { $this->session->logout(); try { - if (!$this->session->logClientIn($user, $pass, $this->request)) { + if (!$this->session->logClientIn($user, $pass, $this->request, $this->throttler)) { throw new SecurityException('CORS requires basic auth', Http::STATUS_UNAUTHORIZED); } } catch (PasswordLoginForbiddenException $ex) { diff --git a/lib/private/Security/Bruteforce/Throttler.php b/lib/private/Security/Bruteforce/Throttler.php new file mode 100644 index 00000000000..0de7677285b --- /dev/null +++ b/lib/private/Security/Bruteforce/Throttler.php @@ -0,0 +1,230 @@ +<?php +/** + * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> + * + * @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/>. + * + */ + +namespace OC\Security\Bruteforce; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\ILogger; + +/** + * Class Throttler implements the bruteforce protection for security actions in + * Nextcloud. + * + * It is working by logging invalid login attempts to the database and slowing + * down all login attempts from the same subnet. The max delay is 30 seconds and + * the starting delay are 200 milliseconds. (after the first failed login) + * + * This is based on Paragonie's AirBrake for Airship CMS. You can find the original + * code at https://github.com/paragonie/airship/blob/7e5bad7e3c0fbbf324c11f963fd1f80e59762606/src/Engine/Security/AirBrake.php + * + * @package OC\Security\Bruteforce + */ +class Throttler { + const LOGIN_ACTION = 'login'; + + /** @var IDBConnection */ + private $db; + /** @var ITimeFactory */ + private $timeFactory; + /** @var ILogger */ + private $logger; + /** @var IConfig */ + private $config; + + /** + * @param IDBConnection $db + * @param ITimeFactory $timeFactory + * @param ILogger $logger + * @param IConfig $config + */ + public function __construct(IDBConnection $db, + ITimeFactory $timeFactory, + ILogger $logger, + IConfig $config) { + $this->db = $db; + $this->timeFactory = $timeFactory; + $this->logger = $logger; + $this->config = $config; + } + + /** + * Convert a number of seconds into the appropriate DateInterval + * + * @param int $expire + * @return \DateInterval + */ + private function getCutoff($expire) { + $d1 = new \DateTime(); + $d2 = clone $d1; + $d2->sub(new \DateInterval('PT' . $expire . 'S')); + return $d2->diff($d1); + } + + /** + * Return the given subnet for an IPv4 address and mask bits + * + * @param string $ip + * @param int $maskBits + * @return string + */ + private function getIPv4Subnet($ip, + $maskBits = 32) { + $binary = \inet_pton($ip); + for ($i = 32; $i > $maskBits; $i -= 8) { + $j = \intdiv($i, 8) - 1; + $k = (int) \min(8, $i - $maskBits); + $mask = (0xff - ((pow(2, $k)) - 1)); + $int = \unpack('C', $binary[$j]); + $binary[$j] = \pack('C', $int[1] & $mask); + } + return \inet_ntop($binary).'/'.$maskBits; + } + + /** + * Return the given subnet for an IPv6 address and mask bits + * + * @param string $ip + * @param int $maskBits + * @return string + */ + private function getIPv6Subnet($ip, $maskBits = 48) { + $binary = \inet_pton($ip); + for ($i = 128; $i > $maskBits; $i -= 8) { + $j = \intdiv($i, 8) - 1; + $k = (int) \min(8, $i - $maskBits); + $mask = (0xff - ((pow(2, $k)) - 1)); + $int = \unpack('C', $binary[$j]); + $binary[$j] = \pack('C', $int[1] & $mask); + } + return \inet_ntop($binary).'/'.$maskBits; + } + + /** + * Return the given subnet for an IP and the configured mask bits + * + * Determine if the IP is an IPv4 or IPv6 address, then pass to the correct + * method for handling that specific type. + * + * @param string $ip + * @return string + */ + private function getSubnet($ip) { + if (\preg_match('/^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/', $ip)) { + return $this->getIPv4Subnet( + $ip, + 32 + ); + } + return $this->getIPv6Subnet( + $ip, + 128 + ); + } + + /** + * Register a failed attempt to bruteforce a security control + * + * @param string $action + * @param string $ip + * @param array $metadata Optional metadata logged to the database + */ + public function registerAttempt($action, + $ip, + array $metadata = []) { + // No need to log if the bruteforce protection is disabled + if($this->config->getSystemValue('auth.bruteforce.protection.enabled', true) === false) { + return; + } + + $values = [ + 'action' => $action, + 'occurred' => $this->timeFactory->getTime(), + 'ip' => $ip, + 'subnet' => $this->getSubnet($ip), + 'metadata' => json_encode($metadata), + ]; + + $this->logger->notice( + sprintf( + 'Bruteforce attempt from "%s" detected for action "%s".', + $ip, + $action + ), + [ + 'app' => 'core', + ] + ); + + $qb = $this->db->getQueryBuilder(); + $qb->insert('bruteforce_attempts'); + foreach($values as $column => $value) { + $qb->setValue($column, $qb->createNamedParameter($value)); + } + $qb->execute(); + } + + /** + * Get the throttling delay (in milliseconds) + * + * @param string $ip + * @return int + */ + public function getDelay($ip) { + $cutoffTime = (new \DateTime()) + ->sub($this->getCutoff(43200)) + ->getTimestamp(); + + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from('bruteforce_attempts') + ->where($qb->expr()->gt('occurred', $qb->createNamedParameter($cutoffTime))) + ->andWhere($qb->expr()->eq('subnet', $qb->createNamedParameter($this->getSubnet($ip)))); + $attempts = count($qb->execute()->fetchAll()); + + if ($attempts === 0) { + return 0; + } + + $maxDelay = 30; + $firstDelay = 0.1; + if ($attempts > (8 * PHP_INT_SIZE - 1)) { + // Don't ever overflow. Just assume the maxDelay time:s + $firstDelay = $maxDelay; + } else { + $firstDelay *= pow(2, $attempts); + if ($firstDelay > $maxDelay) { + $firstDelay = $maxDelay; + } + } + return (int) \ceil($firstDelay * 1000); + } + + /** + * Will sleep for the defined amount of time + * + * @param string $ip + */ + public function sleepDelay($ip) { + usleep($this->getDelay($ip) * 1000); + } +} diff --git a/lib/private/Server.php b/lib/private/Server.php index eb2c26415bc..6ffdeb9211e 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -66,6 +66,7 @@ use OC\Lock\NoopLockingProvider; use OC\Mail\Mailer; use OC\Memcache\ArrayCache; use OC\Notification\Manager; +use OC\Security\Bruteforce\Throttler; use OC\Security\CertificateManager; use OC\Security\CSP\ContentSecurityPolicyManager; use OC\Security\Crypto; @@ -503,6 +504,14 @@ class Server extends ServerContainer implements IServerContainer { $this->registerService('TrustedDomainHelper', function ($c) { return new TrustedDomainHelper($this->getConfig()); }); + $this->registerService('Throttler', function(Server $c) { + return new Throttler( + $c->getDatabaseConnection(), + new TimeFactory(), + $c->getLogger(), + $c->getConfig() + ); + }); $this->registerService('IntegrityCodeChecker', function (Server $c) { // IConfig and IAppManager requires a working database. This code // might however be called when ownCloud is not yet setup. @@ -1331,6 +1340,13 @@ class Server extends ServerContainer implements IServerContainer { } /** + * @return Throttler + */ + public function getBruteForceThrottler() { + return $this->query('Throttler'); + } + + /** * @return IContentSecurityPolicyManager */ public function getContentSecurityPolicyManager() { diff --git a/lib/private/User/Session.php b/lib/private/User/Session.php index dcc2e66c6c3..8d12982dd1a 100644 --- a/lib/private/User/Session.php +++ b/lib/private/User/Session.php @@ -95,7 +95,11 @@ class Session implements IUserSession, Emitter { * @param IProvider $tokenProvider * @param IConfig $config */ - public function __construct(IUserManager $manager, ISession $session, ITimeFactory $timeFacory, $tokenProvider, IConfig $config) { + public function __construct(IUserManager $manager, + ISession $session, + ITimeFactory $timeFacory, + $tokenProvider, + IConfig $config) { $this->manager = $manager; $this->session = $session; $this->timeFacory = $timeFacory; @@ -280,7 +284,6 @@ class Session implements IUserSession, Emitter { */ public function login($uid, $password) { $this->session->regenerateId(); - if ($this->validateToken($password, $uid)) { return $this->loginWithToken($password); } else { @@ -298,11 +301,18 @@ class Session implements IUserSession, Emitter { * @param string $user * @param string $password * @param IRequest $request + * @param OC\Security\Bruteforce\Throttler $throttler * @throws LoginException * @throws PasswordLoginForbiddenException * @return boolean */ - public function logClientIn($user, $password, IRequest $request) { + public function logClientIn($user, + $password, + IRequest $request, + OC\Security\Bruteforce\Throttler $throttler) { + $currentDelay = $throttler->getDelay($request->getRemoteAddress()); + $throttler->sleepDelay($request->getRemoteAddress()); + $isTokenPassword = $this->isTokenPassword($password); if (!$isTokenPassword && $this->isTokenAuthEnforced()) { throw new PasswordLoginForbiddenException(); @@ -315,6 +325,11 @@ class Session implements IUserSession, Emitter { if (count($users) === 1) { return $this->login($users[0]->getUID(), $password); } + + $throttler->registerAttempt('login', $request->getRemoteAddress(), ['uid' => $user]); + if($currentDelay === 0) { + $throttler->sleepDelay($request->getRemoteAddress()); + } return false; } @@ -391,10 +406,11 @@ class Session implements IUserSession, Emitter { * @param IRequest $request * @return boolean if the login was successful */ - public function tryBasicAuthLogin(IRequest $request) { + public function tryBasicAuthLogin(IRequest $request, + OC\Security\Bruteforce\Throttler $throttler) { if (!empty($request->server['PHP_AUTH_USER']) && !empty($request->server['PHP_AUTH_PW'])) { try { - if ($this->logClientIn($request->server['PHP_AUTH_USER'], $request->server['PHP_AUTH_PW'], $request)) { + if ($this->logClientIn($request->server['PHP_AUTH_USER'], $request->server['PHP_AUTH_PW'], $request, $throttler)) { /** * Add DAV authenticated. This should in an ideal world not be * necessary but the iOS App reads cookies from anywhere instead diff --git a/lib/private/legacy/api.php b/lib/private/legacy/api.php index 024f3c0fb63..88eb7b09a78 100644 --- a/lib/private/legacy/api.php +++ b/lib/private/legacy/api.php @@ -364,7 +364,7 @@ class OC_API { try { $loginSuccess = $userSession->tryTokenLogin($request); if (!$loginSuccess) { - $loginSuccess = $userSession->tryBasicAuthLogin($request); + $loginSuccess = $userSession->tryBasicAuthLogin($request, \OC::$server->getBruteForceThrottler()); } } catch (\OC\User\LoginException $e) { return false; diff --git a/tests/Core/Controller/LoginControllerTest.php b/tests/Core/Controller/LoginControllerTest.php index d6fa772d38b..f09f3c98118 100644 --- a/tests/Core/Controller/LoginControllerTest.php +++ b/tests/Core/Controller/LoginControllerTest.php @@ -23,6 +23,7 @@ namespace Tests\Core\Controller; use OC\Authentication\TwoFactorAuth\Manager; use OC\Core\Controller\LoginController; +use OC\Security\Bruteforce\Throttler; use OCP\AppFramework\Http\RedirectResponse; use OCP\AppFramework\Http\TemplateResponse; use OCP\IConfig; @@ -51,6 +52,8 @@ class LoginControllerTest extends TestCase { private $urlGenerator; /** @var Manager | \PHPUnit_Framework_MockObject_MockObject */ private $twoFactorManager; + /** @var Throttler */ + private $throttler; public function setUp() { parent::setUp(); @@ -65,6 +68,9 @@ class LoginControllerTest extends TestCase { $this->twoFactorManager = $this->getMockBuilder('\OC\Authentication\TwoFactorAuth\Manager') ->disableOriginalConstructor() ->getMock(); + $this->throttler = $this->getMockBuilder('\OC\Security\Bruteforce\Throttler') + ->disableOriginalConstructor() + ->getMock(); $this->loginController = new LoginController( 'core', @@ -74,7 +80,8 @@ class LoginControllerTest extends TestCase { $this->session, $this->userSession, $this->urlGenerator, - $this->twoFactorManager + $this->twoFactorManager, + $this->throttler ); } @@ -277,10 +284,27 @@ class LoginControllerTest extends TestCase { } public function testLoginWithInvalidCredentials() { - $user = $this->getMock('\OCP\IUser'); + $user = 'MyUserName'; $password = 'secret'; $loginPageUrl = 'some url'; + $this->request + ->expects($this->exactly(4)) + ->method('getRemoteAddress') + ->willReturn('192.168.0.1'); + $this->throttler + ->expects($this->exactly(2)) + ->method('sleepDelay') + ->with('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('getDelay') + ->with('192.168.0.1') + ->willReturn(0); + $this->throttler + ->expects($this->once()) + ->method('registerAttempt') + ->with('login', '192.168.0.1', ['user' => 'MyUserName']); $this->userManager->expects($this->once()) ->method('checkPassword') ->will($this->returnValue(false)); @@ -302,6 +326,19 @@ class LoginControllerTest extends TestCase { $password = 'secret'; $indexPageUrl = 'some url'; + $this->request + ->expects($this->exactly(2)) + ->method('getRemoteAddress') + ->willReturn('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('sleepDelay') + ->with('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('getDelay') + ->with('192.168.0.1') + ->willReturn(200); $this->userManager->expects($this->once()) ->method('checkPassword') ->will($this->returnValue($user)); @@ -334,6 +371,19 @@ class LoginControllerTest extends TestCase { $originalUrl = 'another%20url'; $redirectUrl = 'http://localhost/another url'; + $this->request + ->expects($this->exactly(2)) + ->method('getRemoteAddress') + ->willReturn('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('sleepDelay') + ->with('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('getDelay') + ->with('192.168.0.1') + ->willReturn(200); $this->userManager->expects($this->once()) ->method('checkPassword') ->with('Jane', $password) @@ -363,6 +413,19 @@ class LoginControllerTest extends TestCase { $password = 'secret'; $challengeUrl = 'challenge/url'; + $this->request + ->expects($this->exactly(2)) + ->method('getRemoteAddress') + ->willReturn('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('sleepDelay') + ->with('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('getDelay') + ->with('192.168.0.1') + ->willReturn(200); $this->userManager->expects($this->once()) ->method('checkPassword') ->will($this->returnValue($user)); @@ -412,6 +475,23 @@ class LoginControllerTest extends TestCase { ->method('linkToRoute') ->with('core.login.showLoginForm', ['user' => 'john@doe.com']) ->will($this->returnValue('')); + $this->request + ->expects($this->exactly(3)) + ->method('getRemoteAddress') + ->willReturn('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('getDelay') + ->with('192.168.0.1') + ->willReturn(200); + $this->throttler + ->expects($this->once()) + ->method('sleepDelay') + ->with('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('registerAttempt') + ->with('login', '192.168.0.1', ['user' => 'john@doe.com']); $expected = new RedirectResponse(''); $this->assertEquals($expected, $this->loginController->tryLogin('john@doe.com', 'just wrong', null)); diff --git a/tests/lib/AppFramework/DependencyInjection/DIContainerTest.php b/tests/lib/AppFramework/DependencyInjection/DIContainerTest.php index 0edf96dd5a4..2e450d897bd 100644 --- a/tests/lib/AppFramework/DependencyInjection/DIContainerTest.php +++ b/tests/lib/AppFramework/DependencyInjection/DIContainerTest.php @@ -29,6 +29,9 @@ namespace Test\AppFramework\DependencyInjection; use \OC\AppFramework\Http\Request; +/** + * @group DB + */ class DIContainerTest extends \Test\TestCase { private $container; @@ -74,7 +77,6 @@ class DIContainerTest extends \Test\TestCase { $this->assertEquals('name', $this->container['AppName']); } - public function testMiddlewareDispatcherIncludesSecurityMiddleware(){ $this->container['Request'] = new Request( ['method' => 'GET'], diff --git a/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php b/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php index a0dbcc6872a..d0096d43f3d 100644 --- a/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php +++ b/tests/lib/AppFramework/Middleware/Security/CORSMiddlewareTest.php @@ -16,6 +16,7 @@ use OC\AppFramework\Http\Request; use OC\AppFramework\Middleware\Security\CORSMiddleware; use OC\AppFramework\Utility\ControllerMethodReflector; use OC\AppFramework\Middleware\Security\Exceptions\SecurityException; +use OC\Security\Bruteforce\Throttler; use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\Response; @@ -24,6 +25,8 @@ class CORSMiddlewareTest extends \Test\TestCase { private $reflector; private $session; + /** @var Throttler */ + private $throttler; protected function setUp() { parent::setUp(); @@ -31,6 +34,9 @@ class CORSMiddlewareTest extends \Test\TestCase { $this->session = $this->getMockBuilder('\OC\User\Session') ->disableOriginalConstructor() ->getMock(); + $this->throttler = $this->getMockBuilder('\OC\Security\Bruteforce\Throttler') + ->disableOriginalConstructor() + ->getMock(); } /** @@ -47,7 +53,7 @@ class CORSMiddlewareTest extends \Test\TestCase { $this->getMockBuilder('\OCP\IConfig')->getMock() ); $this->reflector->reflect($this, __FUNCTION__); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); $response = $middleware->afterController($this, __FUNCTION__, new Response()); $headers = $response->getHeaders(); @@ -65,7 +71,7 @@ class CORSMiddlewareTest extends \Test\TestCase { $this->getMockBuilder('\OCP\Security\ISecureRandom')->getMock(), $this->getMockBuilder('\OCP\IConfig')->getMock() ); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); $response = $middleware->afterController($this, __FUNCTION__, new Response()); $headers = $response->getHeaders(); @@ -83,7 +89,7 @@ class CORSMiddlewareTest extends \Test\TestCase { $this->getMockBuilder('\OCP\IConfig')->getMock() ); $this->reflector->reflect($this, __FUNCTION__); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); $response = $middleware->afterController($this, __FUNCTION__, new Response()); $headers = $response->getHeaders(); @@ -106,7 +112,7 @@ class CORSMiddlewareTest extends \Test\TestCase { $this->getMockBuilder('\OCP\IConfig')->getMock() ); $this->reflector->reflect($this, __FUNCTION__); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); $response = new Response(); $response->addHeader('AcCess-control-Allow-Credentials ', 'TRUE'); @@ -124,7 +130,7 @@ class CORSMiddlewareTest extends \Test\TestCase { $this->getMockBuilder('\OCP\IConfig')->getMock() ); $this->reflector->reflect($this, __FUNCTION__); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); $this->session->expects($this->never()) ->method('logout'); $this->session->expects($this->never()) @@ -155,7 +161,7 @@ class CORSMiddlewareTest extends \Test\TestCase { ->with($this->equalTo('user'), $this->equalTo('pass')) ->will($this->returnValue(true)); $this->reflector->reflect($this, __FUNCTION__); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); $middleware->beforeController($this, __FUNCTION__, new Response()); } @@ -180,7 +186,7 @@ class CORSMiddlewareTest extends \Test\TestCase { ->with($this->equalTo('user'), $this->equalTo('pass')) ->will($this->throwException(new \OC\Authentication\Exceptions\PasswordLoginForbiddenException)); $this->reflector->reflect($this, __FUNCTION__); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); $middleware->beforeController($this, __FUNCTION__, new Response()); } @@ -205,7 +211,7 @@ class CORSMiddlewareTest extends \Test\TestCase { ->with($this->equalTo('user'), $this->equalTo('pass')) ->will($this->returnValue(false)); $this->reflector->reflect($this, __FUNCTION__); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); $middleware->beforeController($this, __FUNCTION__, new Response()); } @@ -219,7 +225,7 @@ class CORSMiddlewareTest extends \Test\TestCase { $this->getMockBuilder('\OCP\Security\ISecureRandom')->getMock(), $this->getMockBuilder('\OCP\IConfig')->getMock() ); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); $response = $middleware->afterException($this, __FUNCTION__, new SecurityException('A security exception')); $expected = new JSONResponse(['message' => 'A security exception'], 500); @@ -235,7 +241,7 @@ class CORSMiddlewareTest extends \Test\TestCase { $this->getMockBuilder('\OCP\Security\ISecureRandom')->getMock(), $this->getMockBuilder('\OCP\IConfig')->getMock() ); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); $response = $middleware->afterException($this, __FUNCTION__, new SecurityException('A security exception', 501)); $expected = new JSONResponse(['message' => 'A security exception'], 501); @@ -255,7 +261,7 @@ class CORSMiddlewareTest extends \Test\TestCase { $this->getMockBuilder('\OCP\Security\ISecureRandom')->getMock(), $this->getMockBuilder('\OCP\IConfig')->getMock() ); - $middleware = new CORSMiddleware($request, $this->reflector, $this->session); + $middleware = new CORSMiddleware($request, $this->reflector, $this->session, $this->throttler); $middleware->afterException($this, __FUNCTION__, new \Exception('A regular exception')); } diff --git a/tests/lib/Security/Bruteforce/ThrottlerTest.php b/tests/lib/Security/Bruteforce/ThrottlerTest.php new file mode 100644 index 00000000000..9b7a47ceec8 --- /dev/null +++ b/tests/lib/Security/Bruteforce/ThrottlerTest.php @@ -0,0 +1,123 @@ +<?php +/** + * @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch> + * + * @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/>. + * + */ +namespace Test\Security\Bruteforce; + +use OC\AppFramework\Utility\TimeFactory; +use OC\Security\Bruteforce\Throttler; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\ILogger; +use Test\TestCase; + +/** + * Based on the unit tests from Paragonie's Airship CMS + * Ref: https://github.com/paragonie/airship/blob/7e5bad7e3c0fbbf324c11f963fd1f80e59762606/test/unit/Engine/Security/AirBrakeTest.php + * + * @package Test\Security\Bruteforce + */ +class ThrottlerTest extends TestCase { + /** @var Throttler */ + private $throttler; + /** @var IDBConnection */ + private $dbConnection; + /** @var ILogger */ + private $logger; + /** @var IConfig */ + private $config; + + public function setUp() { + $this->dbConnection = $this->getMock('\OCP\IDBConnection'); + $this->logger = $this->getMock('\OCP\ILogger'); + $this->config = $this->getMock('\OCP\IConfig'); + + $this->throttler = new Throttler( + $this->dbConnection, + new TimeFactory(), + $this->logger, + $this->config + ); + return parent::setUp(); + } + + public function testCutoff() { + // precisely 31 second shy of 12 hours + $cutoff = $this->invokePrivate($this->throttler, 'getCutoff', [43169]); + $this->assertSame(0, $cutoff->y); + $this->assertSame(0, $cutoff->m); + $this->assertSame(0, $cutoff->d); + $this->assertSame(11, $cutoff->h); + $this->assertSame(59, $cutoff->i); + $this->assertSame(29, $cutoff->s); + $cutoff = $this->invokePrivate($this->throttler, 'getCutoff', [86401]); + $this->assertSame(0, $cutoff->y); + $this->assertSame(0, $cutoff->m); + $this->assertSame(1, $cutoff->d); + $this->assertSame(0, $cutoff->h); + $this->assertSame(0, $cutoff->i); + // Leap second tolerance: + $this->assertLessThan(2, $cutoff->s); + } + + public function testSubnet() { + // IPv4 + $this->assertSame( + '64.233.191.254/32', + $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 32]) + ); + $this->assertSame( + '64.233.191.252/30', + $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 30]) + ); + $this->assertSame( + '64.233.191.240/28', + $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 28]) + ); + $this->assertSame( + '64.233.191.0/24', + $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 24]) + ); + $this->assertSame( + '64.233.188.0/22', + $this->invokePrivate($this->throttler, 'getIPv4Subnet', ['64.233.191.254', 22]) + ); + // IPv6 + $this->assertSame( + '2001:db8:85a3::8a2e:370:7334/127', + $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7334', 127]) + ); + $this->assertSame( + '2001:db8:85a3::8a2e:370:7300/120', + $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7300', 120]) + ); + $this->assertSame( + '2001:db8:85a3::/64', + $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7334', 64]) + ); + $this->assertSame( + '2001:db8:85a3::/48', + $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7334', 48]) + ); + $this->assertSame( + '2001:db8:8500::/40', + $this->invokePrivate($this->throttler, 'getIPv6Subnet', ['2001:0db8:85a3:0000:0000:8a2e:0370:7334', 40]) + ); + } +} diff --git a/tests/lib/User/SessionTest.php b/tests/lib/User/SessionTest.php index 9bde2c664b6..379c7e39442 100644 --- a/tests/lib/User/SessionTest.php +++ b/tests/lib/User/SessionTest.php @@ -9,6 +9,7 @@ namespace Test\User; +use OC\Security\Bruteforce\Throttler; use OC\Session\Memory; use OC\User\User; @@ -17,15 +18,14 @@ use OC\User\User; * @package Test\User */ class SessionTest extends \Test\TestCase { - /** @var \OCP\AppFramework\Utility\ITimeFactory */ private $timeFactory; - /** @var \OC\Authentication\Token\DefaultTokenProvider */ protected $tokenProvider; - /** @var \OCP\IConfig */ private $config; + /** @var Throttler */ + private $throttler; protected function setUp() { parent::setUp(); @@ -36,6 +36,8 @@ class SessionTest extends \Test\TestCase { ->will($this->returnValue(10000)); $this->tokenProvider = $this->getMock('\OC\Authentication\Token\IProvider'); $this->config = $this->getMock('\OCP\IConfig'); + $this->throttler = $this->getMockBuilder('\OC\Security\Bruteforce\Throttler') + ->disableOriginalConstructor()->getMock(); } public function testGetUser() { @@ -353,7 +355,6 @@ class SessionTest extends \Test\TestCase { ->getMock(); $session = $this->getMock('\OCP\ISession'); $request = $this->getMock('\OCP\IRequest'); - $user = $this->getMock('\OCP\IUser'); /** @var \OC\User\Session $userSession */ $userSession = $this->getMockBuilder('\OC\User\Session') @@ -369,8 +370,21 @@ class SessionTest extends \Test\TestCase { ->method('getSystemValue') ->with('token_auth_enforced', false) ->will($this->returnValue(true)); - - $userSession->logClientIn('john', 'doe', $request); + $request + ->expects($this->exactly(2)) + ->method('getRemoteAddress') + ->willReturn('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('sleepDelay') + ->with('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('getDelay') + ->with('192.168.0.1') + ->willReturn(0); + + $userSession->logClientIn('john', 'doe', $request, $this->throttler); } public function testLogClientInWithTokenPassword() { @@ -379,7 +393,6 @@ class SessionTest extends \Test\TestCase { ->getMock(); $session = $this->getMock('\OCP\ISession'); $request = $this->getMock('\OCP\IRequest'); - $user = $this->getMock('\OCP\IUser'); /** @var \OC\User\Session $userSession */ $userSession = $this->getMockBuilder('\OC\User\Session') @@ -398,8 +411,21 @@ class SessionTest extends \Test\TestCase { $session->expects($this->once()) ->method('set') ->with('app_password', 'I-AM-AN-APP-PASSWORD'); - - $this->assertTrue($userSession->logClientIn('john', 'I-AM-AN-APP-PASSWORD', $request)); + $request + ->expects($this->exactly(2)) + ->method('getRemoteAddress') + ->willReturn('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('sleepDelay') + ->with('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('getDelay') + ->with('192.168.0.1') + ->willReturn(0); + + $this->assertTrue($userSession->logClientIn('john', 'I-AM-AN-APP-PASSWORD', $request, $this->throttler)); } /** @@ -410,7 +436,6 @@ class SessionTest extends \Test\TestCase { ->disableOriginalConstructor() ->getMock(); $session = $this->getMock('\OCP\ISession'); - $user = $this->getMock('\OCP\IUser'); $request = $this->getMock('\OCP\IRequest'); /** @var \OC\User\Session $userSession */ @@ -433,7 +458,21 @@ class SessionTest extends \Test\TestCase { ->with('john') ->will($this->returnValue(true)); - $userSession->logClientIn('john', 'doe', $request); + $request + ->expects($this->exactly(2)) + ->method('getRemoteAddress') + ->willReturn('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('sleepDelay') + ->with('192.168.0.1'); + $this->throttler + ->expects($this->once()) + ->method('getDelay') + ->with('192.168.0.1') + ->willReturn(0); + + $userSession->logClientIn('john', 'doe', $request, $this->throttler); } public function testRememberLoginValidToken() { diff --git a/version.php b/version.php index 15400c4f4e5..5f88d29b155 100644 --- a/version.php +++ b/version.php @@ -25,7 +25,7 @@ // We only can count up. The 4. digit is only for the internal patchlevel to trigger DB upgrades // between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel // when updating major/minor version number. -$OC_Version = array(9, 1, 0, 12); +$OC_Version = array(9, 1, 0, 13); // The human readable string $OC_VersionString = '9.1.0 RC1'; |