diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a115a46109..f6f7ff9354 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -815,6 +815,8 @@ phutil_register_library_map(array( 'PhabricatorAuditReplyHandler' => 'applications/audit/mail/PhabricatorAuditReplyHandler.php', 'PhabricatorAuditStatusConstants' => 'applications/audit/constants/PhabricatorAuditStatusConstants.php', 'PhabricatorAuthController' => 'applications/auth/controller/PhabricatorAuthController.php', + 'PhabricatorAuthLoginController' => 'applications/auth/controller/PhabricatorAuthLoginController.php', + 'PhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorAuthProvider.php', 'PhabricatorAuthRegisterController' => 'applications/auth/controller/PhabricatorAuthRegisterController.php', 'PhabricatorAuthenticationConfigOptions' => 'applications/config/option/PhabricatorAuthenticationConfigOptions.php', 'PhabricatorBarePageExample' => 'applications/uiexample/examples/PhabricatorBarePageExample.php', @@ -2669,6 +2671,7 @@ phutil_register_library_map(array( 'PhabricatorAuditPreviewController' => 'PhabricatorAuditController', 'PhabricatorAuditReplyHandler' => 'PhabricatorMailReplyHandler', 'PhabricatorAuthController' => 'PhabricatorController', + 'PhabricatorAuthLoginController' => 'PhabricatorAuthController', 'PhabricatorAuthRegisterController' => 'PhabricatorAuthController', 'PhabricatorAuthenticationConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorBarePageExample' => 'PhabricatorUIExample', diff --git a/src/applications/auth/application/PhabricatorApplicationAuth.php b/src/applications/auth/application/PhabricatorApplicationAuth.php index 6fcce8ac23..253bddd997 100644 --- a/src/applications/auth/application/PhabricatorApplicationAuth.php +++ b/src/applications/auth/application/PhabricatorApplicationAuth.php @@ -32,6 +32,7 @@ final class PhabricatorApplicationAuth extends PhabricatorApplication { public function getRoutes() { return array( '/auth/' => array( + 'login/(?P[^/]+)/' => 'PhabricatorAuthLoginController', 'register/(?P[^/]+)/' => 'PhabricatorAuthRegisterController', ), ); diff --git a/src/applications/auth/controller/PhabricatorAuthLoginController.php b/src/applications/auth/controller/PhabricatorAuthLoginController.php new file mode 100644 index 0000000000..bd5420b030 --- /dev/null +++ b/src/applications/auth/controller/PhabricatorAuthLoginController.php @@ -0,0 +1,160 @@ +providerKey = $data['pkey']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $response = $this->loadProvider(); + if ($response) { + return $response; + } + + $provider = $this->provider; + + list($account, $response) = $provider->processLoginRequest($this); + if ($response) { + return $response; + } + + if ($account->getUserPHID()) { + // The account is already attached to a Phabricator user, so this is + // either a login or a bad account link request. + if (!$viewer->isLoggedIn()) { + if ($provider->shouldAllowLogin()) { + return $this->processLoginUser($account); + } else { + return $this->renderError( + pht( + 'The external account ("%s") you just authenticated with is '. + 'not configured to allow logins on this Phabricator install. '. + 'An administrator may have recently disabled it.', + $provider->getProviderName())); + } + } else if ($viewer->getPHID() == $account->getUserPHID()) { + return $this->renderError( + pht( + 'This external account ("%s") is already linked to your '. + 'Phabricator account.')); + } else { + return $this->renderError( + pht( + 'The external account ("%s") you just used to login is alerady '. + 'associated with another Phabricator user account. Login to the '. + 'other Phabricator account and unlink the external account before '. + 'linking it to a new Phabricator account.', + $provider->getProviderName())); + } + } else { + // The account is not yet attached to a Phabricator user, so this is + // either a registration or an account link request. + if (!$viewer->isLoggedIn()) { + if ($provider->shouldAllowRegistration()) { + return $this->processRegisterUser($account); + } else { + return $this->renderError( + pht( + 'The external account ("%s") you just authenticated with is '. + 'not configured to allow registration on this Phabricator '. + 'install. An administrator may have recently disabled it.', + $provider->getProviderName())); + } + } else { + if ($provider->shouldAllowAccountLink()) { + return $this->processLinkUser($account); + } else { + return $this->renderError( + pht( + 'The external account ("%s") you just authenticated with is '. + 'not configured to allow account linking on this Phabricator '. + 'install. An administrator may have recently disabled it.')); + } + } + } + + // This should be unreachable, but fail explicitly if we get here somehow. + return new Aphront400Response(); + } + + private function processLoginUser(PhabricatorExternalAccount $account) { + // TODO: Implement. + return new Aphront404Response(); + } + + private function processRegisterUser(PhabricatorExternalAccount $account) { + if ($account->getUserPHID()) { + throw new Exception("Account is already registered."); + } + + // Regenerate the registration secret key, set it on the external account, + // set a cookie on the user's machine, and redirect them to registration. + // See PhabricatorAuthRegisterController for discussion of the registration + // key. + + $registration_key = Filesystem::readRandomCharacters(32); + $account->setProperty('registrationKey', $registration_key); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $account->save(); + unset($unguarded); + + $this->getRequest()->setCookie('phreg', $registration_key); + + $account_secret = $account->getAccountSecret(); + $register_uri = $this->getApplicationURI('register/'.$account_secret.'/'); + return id(new AphrontRedirectResponse())->setURI($register_uri); + } + + private function processLinkUser(PhabricatorExternalAccount $account) { + // TODO: Implement. + return new Aphront404Response(); + } + + private function loadProvider() { + $provider = PhabricatorAuthProvider::getEnabledProviderByKey( + $this->providerKey); + + if (!$provider) { + return $this->renderError( + pht( + 'The account you are attempting to login with uses a nonexistent '. + 'or disabled authentication provider (with key "%s"). An '. + 'administrator may have recently disabled this provider.', + $this->providerKey)); + } + + $this->provider = $provider; + + return null; + } + + private function renderError($message) { + $title = pht('Login Failed'); + + $view = new AphrontErrorView(); + $view->setTitle($title); + $view->appendChild($message); + + return $this->buildApplicationPage( + $view, + array( + 'title' => $title, + 'device' => true, + 'dust' => true, + )); + } + +} diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php new file mode 100644 index 0000000000..987d549960 --- /dev/null +++ b/src/applications/auth/provider/PhabricatorAuthProvider.php @@ -0,0 +1,81 @@ +adapter = $adapter; + return $this; + } + + public function getAdapater() { + if ($this->adapter === null) { + throw new Exception("Call setAdapter() before getAdapter()!"); + } + return $this->adapter; + } + + public function getProviderKey() { + return $this->getAdapter()->getAdapterKey(); + } + + public static function getAllProviders() { + static $providers; + + if ($providers === null) { + $objects = id(new PhutilSymbolLoader()) + ->setAncestorClass(__CLASS__) + ->loadObjects(); + + $providers = array(); + $from_class_map = array(); + foreach ($objects as $object) { + $from_class = get_class($object); + $object_providers = $object->createProviders(); + assert_instances_of($object_providers, 'PhabricatorAuthProvider'); + foreach ($object_providers as $provider) { + $key = $provider->getProviderKey(); + if (isset($providers[$key])) { + $first_class = $from_class_map[$key]; + throw new Exception( + "PhabricatorAuthProviders '{$first_class}' and '{$from_class}' ". + "both created authentication providers identified by key ". + "'{$key}'. Provider keys must be unique."); + } + $providers[$key] = $provider; + $from_class_map[$key] = $from_class; + } + } + } + + return $providers; + } + + public static function getEnabledProviders() { + $providers = self::getAllProviders(); + foreach ($providers as $key => $provider) { + if (!$provider->isEnabled()) { + unset($providers[$key]); + } + } + return $providers; + } + + public static function getEnabledProviderByKey($provider_key) { + return idx(self::getEnabledProviders(), $provider_key); + } + + abstract public function getProviderName(); + abstract public function isEnabled(); + abstract public function shouldAllowLogin(); + abstract public function shouldAllowRegistration(); + abstract public function shouldAllowAccountLink(); + abstract public function processLoginRequest( + PhabricatorAuthLoginController $controller); + + public function createProviders() { + return array($this); + } + +} diff --git a/src/applications/people/storage/PhabricatorExternalAccount.php b/src/applications/people/storage/PhabricatorExternalAccount.php index 5764f87d9b..9fb252f0db 100644 --- a/src/applications/people/storage/PhabricatorExternalAccount.php +++ b/src/applications/people/storage/PhabricatorExternalAccount.php @@ -37,6 +37,10 @@ final class PhabricatorExternalAccount extends PhabricatorUserDAO { return $tmp_usr; } + public function getProviderKey() { + return $this->getAccountType().':'.$this->accountDomain(); + } + public function save() { if (!$this->getAccountSecret()) { $this->setAccountSecret(Filesystem::readRandomCharacters(32)); @@ -44,4 +48,13 @@ final class PhabricatorExternalAccount extends PhabricatorUserDAO { return parent::save(); } + public function setProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + + public function getProperty($key, $default = null) { + return idx($this->properties, $key, $default); + } + }