diff --git a/src/applications/auth/controller/PhabricatorAuthLoginController.php b/src/applications/auth/controller/PhabricatorAuthLoginController.php index bd5420b030..44eceec8a8 100644 --- a/src/applications/auth/controller/PhabricatorAuthLoginController.php +++ b/src/applications/auth/controller/PhabricatorAuthLoginController.php @@ -1,160 +1,172 @@ <?php final class PhabricatorAuthLoginController extends PhabricatorAuthController { private $providerKey; private $provider; public function shouldRequireLogin() { return false; } public function willProcessRequest(array $data) { $this->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, )); } + public function buildProviderErrorResponse( + PhabricatorAuthProvider $provider, + $message) { + + $message = pht( + 'Authentication provider ("%s") encountered an error during login. %s', + $provider->getProviderName(), + $message); + + return $this->renderError($message); + } + } diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php index 987d549960..3169bc2678 100644 --- a/src/applications/auth/provider/PhabricatorAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorAuthProvider.php @@ -1,81 +1,125 @@ <?php abstract class PhabricatorAuthProvider { - private $adapter; - - public function setAdapater(PhutilAuthAdapter $adapter) { - $this->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 getAdapater(); 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); } + protected function willSaveAccount(PhabricatorExternalAccount $account) { + return; + } + + protected function loadOrCreateAccount($account_id) { + if (!strlen($account_id)) { + throw new Exception("loadOrCreateAccount(...): empty account ID!"); + } + + $adapter = $this->getAdapter(); + $account = id(new PhabricatorExternalAccount())->loadOneWhere( + 'accountType = %s AND accountDomain = %s AND accountID = %s', + $adapter->getProviderType(), + $adapter->getProviderDomain(), + $account_id); + if (!$account) { + $account = id(new PhabricatorExternalAccount()) + ->setAccountType($adapter->getProviderType()) + ->setAccountDomain($adapter->getProviderDomain()) + ->setAccountID($account_id); + } + + $account->setUsername($adapter->getAccountName()); + $account->setRealName($adapter->getAccountRealName()); + $account->setEmail($adapter->getAccountEmail()); + $account->setAccountURI($adapter->getAccountURI()); + + try { + $name = PhabricatorSlug::normalize($this->getProviderName()); + $name = $name.'-profile.jpg'; + + // TODO: If the image has not changed, we do not need to make a new + // file entry for it, but there's no convenient way to do this with + // PhabricatorFile right now. The storage will get shared, so the impact + // here is negligible. + + $image_uri = $account->getAccountImageURI(); + $image_file = PhabricatorFile::newFromFileDownload( + $image_uri, + array( + 'name' => $name, + )); + + $account->setProfileImagePHID($image_file->getPHID()); + } catch (Exception $ex) { + $account->setProfileImagePHID(null); + } + + $this->willSaveAccount($account); + + $account->save(); + + return $account; + } + + + } diff --git a/src/applications/auth/provider/PhabricatorAuthProviderOAuth.php b/src/applications/auth/provider/PhabricatorAuthProviderOAuth.php new file mode 100644 index 0000000000..d4fbd08f49 --- /dev/null +++ b/src/applications/auth/provider/PhabricatorAuthProviderOAuth.php @@ -0,0 +1,59 @@ +<?php + +abstract class PhabricatorAuthProviderOAuth extends PhabricatorAuthProvider { + + public function processLoginRequest( + PhabricatorAuthLoginController $controller) { + + $request = $controller->getRequest(); + $adapter = $this->getAdapter(); + $account = null; + $response = null; + + $error = $request->getStr('error'); + if ($error) { + $response = $controller->buildProviderErrorResponse( + $this, + pht( + 'The OAuth provider returned an error: %s', + $error)); + + return array($account, $response); + } + + $code = $request->getStr('code'); + if (!strlen($code)) { + $response = $controller->buildProviderErrorResponse( + $this, + pht( + 'The OAuth provider did not return a "code" parameter in its '. + 'response.')); + + return array($account, $response); + } + + $adapter->setCode($code); + + // NOTE: As a side effect, this will cause the OAuth adapter to request + // an access token. + + try { + $account_id = $adapter->getAccountID(); + } catch (Exception $ex) { + // TODO: Handle this in a more user-friendly way. + throw $ex; + } + + if (!strlen($account_id)) { + $response = $controller->buildProviderErrorResponse( + $this, + pht( + 'The OAuth provider failed to retrieve an account ID.')); + + return array($account, $response); + } + + return array($this->loadOrCreateAccount($account_id), $response); + } + +}