diff --git a/conf/default.conf.php b/conf/default.conf.php index 3335464354..72a3e48e35 100644 --- a/conf/default.conf.php +++ b/conf/default.conf.php @@ -390,7 +390,9 @@ return array( // -- Recaptcha ------------------------------------------------------------- // - // Is Recaptcha enabled? If disabled, captchas will not appear. + // Is Recaptcha enabled? If disabled, captchas will not appear. You should + // enable Recaptcha if your install is public-facing, as it hinders + // brute-force attacks. 'recaptcha.enabled' => false, // Your Recaptcha public key, obtained from Recaptcha. diff --git a/src/applications/auth/controller/login/PhabricatorLoginController.php b/src/applications/auth/controller/login/PhabricatorLoginController.php index 86089dde06..f5f9967df4 100644 --- a/src/applications/auth/controller/login/PhabricatorLoginController.php +++ b/src/applications/auth/controller/login/PhabricatorLoginController.php @@ -43,11 +43,33 @@ class PhabricatorLoginController extends PhabricatorAuthController { $forms = array(); - $error_view = null; + + $errors = array(); if ($password_auth) { - $error = false; + $require_captcha = false; + $e_captcha = true; $username_or_email = $request->getCookie('phusr'); if ($request->isFormPost()) { + + if (AphrontFormRecaptchaControl::isRecaptchaEnabled()) { + $failed_attempts = PhabricatorUserLog::loadRecentEventsFromThisIP( + PhabricatorUserLog::ACTION_LOGIN_FAILURE, + 60 * 15); + if (count($failed_attempts) > 5) { + $require_captcha = true; + if (!AphrontFormRecaptchaControl::processCaptcha($request)) { + if (AphrontFormRecaptchaControl::hasCaptchaResponse($request)) { + $e_captcha = 'Invalid'; + $errors[] = 'CAPTCHA was not entered correctly.'; + } else { + $e_captcha = 'Required'; + $errors[] = 'Too many login failures recently. You must '. + 'submit a CAPTCHA with your login request.'; + } + } + } + } + $username_or_email = $request->getStr('username_or_email'); $user = id(new PhabricatorUser())->loadOneWhere( @@ -60,43 +82,46 @@ class PhabricatorLoginController extends PhabricatorAuthController { $username_or_email); } - $okay = false; - if ($user) { - if ($user->comparePassword($request->getStr('password'))) { - - $session_key = $user->establishSession('web'); - - $request->setCookie('phusr', $user->getUsername()); - $request->setCookie('phsid', $session_key); - - $uri = new PhutilURI('/login/validate/'); - $uri->setQueryParams( - array( - 'phusr' => $user->getUsername(), - )); - - return id(new AphrontRedirectResponse()) - ->setURI((string)$uri); - } else { - $log = PhabricatorUserLog::newLog( - null, - $user, - PhabricatorUserLog::ACTION_LOGIN_FAILURE); - $log->save(); + if (!$errors) { + // Perform username/password tests only if we didn't get rate limited + // by the CAPTCHA. + if (!$user || !$user->comparePassword($request->getStr('password'))) { + $errors[] = 'Bad username/password.'; } } - if (!$okay) { + if (!$errors) { + $session_key = $user->establishSession('web'); + + $request->setCookie('phusr', $user->getUsername()); + $request->setCookie('phsid', $session_key); + + $uri = new PhutilURI('/login/validate/'); + $uri->setQueryParams( + array( + 'phusr' => $user->getUsername(), + )); + + return id(new AphrontRedirectResponse()) + ->setURI((string)$uri); + } else { + $log = PhabricatorUserLog::newLog( + null, + $user, + PhabricatorUserLog::ACTION_LOGIN_FAILURE); + $log->save(); + $request->clearCookie('phusr'); $request->clearCookie('phsid'); } - - $error = true; } - if ($error) { + if ($errors) { $error_view = new AphrontErrorView(); - $error_view->setTitle('Bad username/password.'); + $error_view->setTitle('Login Failed'); + $error_view->setErrors($errors); + } else { + $error_view = null; } $form = new AphrontFormView(); @@ -114,7 +139,15 @@ class PhabricatorLoginController extends PhabricatorAuthController { ->setName('password') ->setCaption( ''. - 'Forgot your password? / Email Login')) + 'Forgot your password? / Email Login')); + + if ($require_captcha) { + $form->appendChild( + id(new AphrontFormRecaptchaControl()) + ->setError($e_captcha)); + } + + $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Login')); diff --git a/src/applications/auth/controller/login/__init__.php b/src/applications/auth/controller/login/__init__.php index 3d05f76225..d452c764bd 100644 --- a/src/applications/auth/controller/login/__init__.php +++ b/src/applications/auth/controller/login/__init__.php @@ -14,6 +14,7 @@ phutil_require_module('phabricator', 'applications/people/storage/user'); phutil_require_module('phabricator', 'infrastructure/env'); phutil_require_module('phabricator', 'view/form/base'); phutil_require_module('phabricator', 'view/form/control/password'); +phutil_require_module('phabricator', 'view/form/control/recaptcha'); phutil_require_module('phabricator', 'view/form/control/submit'); phutil_require_module('phabricator', 'view/form/control/text'); phutil_require_module('phabricator', 'view/form/error'); diff --git a/src/applications/people/storage/log/PhabricatorUserLog.php b/src/applications/people/storage/log/PhabricatorUserLog.php index 339877e4bb..8d3be1eed8 100644 --- a/src/applications/people/storage/log/PhabricatorUserLog.php +++ b/src/applications/people/storage/log/PhabricatorUserLog.php @@ -1,7 +1,7 @@ setUserPHID($user->getPHID()); + } else { + $log->setUserPHID(''); } if ($action) { diff --git a/src/view/form/control/recaptcha/AphrontFormRecaptchaControl.php b/src/view/form/control/recaptcha/AphrontFormRecaptchaControl.php index d2c5772f02..7d2c0e715f 100644 --- a/src/view/form/control/recaptcha/AphrontFormRecaptchaControl.php +++ b/src/view/form/control/recaptcha/AphrontFormRecaptchaControl.php @@ -31,7 +31,7 @@ class AphrontFormRecaptchaControl extends AphrontFormControl { return self::isRecaptchaEnabled(); } - private static function isRecaptchaEnabled() { + public static function isRecaptchaEnabled() { return PhabricatorEnv::getEnvConfig('recaptcha.enabled'); } @@ -40,6 +40,10 @@ class AphrontFormRecaptchaControl extends AphrontFormControl { require_once dirname($root).'/externals/recaptcha/recaptchalib.php'; } + public static function hasCaptchaResponse(AphrontRequest $request) { + return $request->getBool('recaptcha_response_field'); + } + public static function processCaptcha(AphrontRequest $request) { if (!self::isRecaptchaEnabled()) { return true;