diff --git a/src/applications/auth/provider/PhabricatorOAuth1AuthProvider.php b/src/applications/auth/provider/PhabricatorOAuth1AuthProvider.php index 7133cff042..672772eab3 100644 --- a/src/applications/auth/provider/PhabricatorOAuth1AuthProvider.php +++ b/src/applications/auth/provider/PhabricatorOAuth1AuthProvider.php @@ -1,285 +1,285 @@ <?php abstract class PhabricatorOAuth1AuthProvider extends PhabricatorOAuthAuthProvider { protected $adapter; const PROPERTY_CONSUMER_KEY = 'oauth1:consumer:key'; const PROPERTY_CONSUMER_SECRET = 'oauth1:consumer:secret'; const PROPERTY_PRIVATE_KEY = 'oauth1:private:key'; protected function getIDKey() { return self::PROPERTY_CONSUMER_KEY; } protected function getSecretKey() { return self::PROPERTY_CONSUMER_SECRET; } protected function configureAdapter(PhutilOAuth1AuthAdapter $adapter) { $config = $this->getProviderConfig(); $adapter->setConsumerKey($config->getProperty(self::PROPERTY_CONSUMER_KEY)); $secret = $config->getProperty(self::PROPERTY_CONSUMER_SECRET); if (phutil_nonempty_string($secret)) { $adapter->setConsumerSecret(new PhutilOpaqueEnvelope($secret)); } $adapter->setCallbackURI(PhabricatorEnv::getURI($this->getLoginURI())); return $adapter; } protected function renderLoginForm(AphrontRequest $request, $mode) { $attributes = array( 'method' => 'POST', 'uri' => $this->getLoginURI(), ); return $this->renderStandardLoginButton($request, $mode, $attributes); } public function processLoginRequest( PhabricatorAuthLoginController $controller) { $request = $controller->getRequest(); $adapter = $this->getAdapter(); $account = null; $response = null; if ($request->isHTTPPost()) { // Add a CSRF code to the callback URI, which we'll verify when // performing the login. $client_code = $this->getAuthCSRFCode($request); $callback_uri = $adapter->getCallbackURI(); $callback_uri = $callback_uri.$client_code.'/'; $adapter->setCallbackURI($callback_uri); $uri = $adapter->getClientRedirectURI(); $this->saveHandshakeTokenSecret( $client_code, $adapter->getTokenSecret()); $response = id(new AphrontRedirectResponse()) ->setIsExternal(true) ->setURI($uri); return array($account, $response); } $denied = $request->getStr('denied'); if ($denied) { // Twitter indicates that the user cancelled the login attempt by // returning "denied" as a parameter. throw new PhutilAuthUserAbortedException(); } // NOTE: You can get here via GET, this should probably be a bit more // user friendly. $this->verifyAuthCSRFCode($request, $controller->getExtraURIData()); $token = $request->getStr('oauth_token'); $verifier = $request->getStr('oauth_verifier'); if (!$token) { throw new Exception(pht("Expected '%s' in request!", 'oauth_token')); } if (!$verifier) { throw new Exception(pht("Expected '%s' in request!", 'oauth_verifier')); } $adapter->setToken($token); $adapter->setVerifier($verifier); $client_code = $this->getAuthCSRFCode($request); $token_secret = $this->loadHandshakeTokenSecret($client_code); $adapter->setTokenSecret($token_secret); // NOTE: As a side effect, this will cause the OAuth adapter to request // an access token. try { $identifiers = $adapter->getAccountIdentifiers(); } catch (Exception $ex) { // TODO: Handle this in a more user-friendly way. throw $ex; } if (!$identifiers) { $response = $controller->buildProviderErrorResponse( $this, pht( 'The OAuth provider failed to retrieve an account ID.')); return array($account, $response); } $account = $this->newExternalAccountForIdentifiers($identifiers); return array($account, $response); } public function processEditForm( AphrontRequest $request, array $values) { $key_ckey = self::PROPERTY_CONSUMER_KEY; $key_csecret = self::PROPERTY_CONSUMER_SECRET; return $this->processOAuthEditForm( $request, $values, pht('Consumer key is required.'), pht('Consumer secret is required.')); } public function extendEditForm( AphrontRequest $request, AphrontFormView $form, array $values, array $issues) { return $this->extendOAuthEditForm( $request, $form, $values, $issues, pht('OAuth Consumer Key'), pht('OAuth Consumer Secret')); } public function renderConfigPropertyTransactionTitle( PhabricatorAuthProviderConfigTransaction $xaction) { $author_phid = $xaction->getAuthorPHID(); $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $key = $xaction->getMetadataValue( PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY); switch ($key) { case self::PROPERTY_CONSUMER_KEY: - if (strlen($old)) { + if (phutil_nonempty_string($old)) { return pht( '%s updated the OAuth consumer key for this provider from '. '"%s" to "%s".', $xaction->renderHandleLink($author_phid), $old, $new); } else { return pht( '%s set the OAuth consumer key for this provider to '. '"%s".', $xaction->renderHandleLink($author_phid), $new); } case self::PROPERTY_CONSUMER_SECRET: - if (strlen($old)) { + if (phutil_nonempty_string($old)) { return pht( '%s updated the OAuth consumer secret for this provider.', $xaction->renderHandleLink($author_phid)); } else { return pht( '%s set the OAuth consumer secret for this provider.', $xaction->renderHandleLink($author_phid)); } } return parent::renderConfigPropertyTransactionTitle($xaction); } protected function synchronizeOAuthAccount( PhabricatorExternalAccount $account) { $adapter = $this->getAdapter(); $oauth_token = $adapter->getToken(); $oauth_token_secret = $adapter->getTokenSecret(); $account->setProperty('oauth1.token', $oauth_token); $account->setProperty('oauth1.token.secret', $oauth_token_secret); } public function willRenderLinkedAccount( PhabricatorUser $viewer, PHUIObjectItemView $item, PhabricatorExternalAccount $account) { $item->addAttribute(pht('OAuth1 Account')); parent::willRenderLinkedAccount($viewer, $item, $account); } protected function getContentSecurityPolicyFormActions() { return $this->getAdapter()->getContentSecurityPolicyFormActions(); } /* -( Temporary Secrets )-------------------------------------------------- */ private function saveHandshakeTokenSecret($client_code, $secret) { $secret_type = PhabricatorOAuth1SecretTemporaryTokenType::TOKENTYPE; $key = $this->getHandshakeTokenKeyFromClientCode($client_code); $type = $this->getTemporaryTokenType($secret_type); // Wipe out an existing token, if one exists. $token = id(new PhabricatorAuthTemporaryTokenQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withTokenResources(array($key)) ->withTokenTypes(array($type)) ->executeOne(); if ($token) { $token->delete(); } // Save the new secret. id(new PhabricatorAuthTemporaryToken()) ->setTokenResource($key) ->setTokenType($type) ->setTokenExpires(time() + phutil_units('1 hour in seconds')) ->setTokenCode($secret) ->save(); } private function loadHandshakeTokenSecret($client_code) { $secret_type = PhabricatorOAuth1SecretTemporaryTokenType::TOKENTYPE; $key = $this->getHandshakeTokenKeyFromClientCode($client_code); $type = $this->getTemporaryTokenType($secret_type); $token = id(new PhabricatorAuthTemporaryTokenQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withTokenResources(array($key)) ->withTokenTypes(array($type)) ->withExpired(false) ->executeOne(); if (!$token) { throw new Exception( pht( 'Unable to load your OAuth1 token secret from storage. It may '. 'have expired. Try authenticating again.')); } return $token->getTokenCode(); } private function getTemporaryTokenType($core_type) { // Namespace the type so that multiple providers don't step on each // others' toes if a user starts Mediawiki and Bitbucket auth at the // same time. // TODO: This isn't really a proper use of the table and should get // cleaned up some day: the type should be constant. return $core_type.':'.$this->getProviderConfig()->getID(); } private function getHandshakeTokenKeyFromClientCode($client_code) { // NOTE: This is very slightly coercive since the TemporaryToken table // expects an "objectPHID" as an identifier, but nothing about the storage // is bound to PHIDs. return 'oauth1:secret/'.$client_code; } } diff --git a/src/applications/auth/provider/PhabricatorOAuthAuthProvider.php b/src/applications/auth/provider/PhabricatorOAuthAuthProvider.php index df76c655f3..f813a37c43 100644 --- a/src/applications/auth/provider/PhabricatorOAuthAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorOAuthAuthProvider.php @@ -1,170 +1,170 @@ <?php abstract class PhabricatorOAuthAuthProvider extends PhabricatorAuthProvider { const PROPERTY_NOTE = 'oauth:app:note'; protected $adapter; abstract protected function newOAuthAdapter(); abstract protected function getIDKey(); abstract protected function getSecretKey(); public function getDescriptionForCreate() { return pht('Configure %s OAuth.', $this->getProviderName()); } public function getAdapter() { if (!$this->adapter) { $adapter = $this->newOAuthAdapter(); $this->adapter = $adapter; $this->configureAdapter($adapter); } return $this->adapter; } public function isLoginFormAButton() { return true; } public function readFormValuesFromProvider() { $config = $this->getProviderConfig(); $id = $config->getProperty($this->getIDKey()); $secret = $config->getProperty($this->getSecretKey()); $note = $config->getProperty(self::PROPERTY_NOTE); return array( $this->getIDKey() => $id, $this->getSecretKey() => $secret, self::PROPERTY_NOTE => $note, ); } public function readFormValuesFromRequest(AphrontRequest $request) { return array( $this->getIDKey() => $request->getStr($this->getIDKey()), $this->getSecretKey() => $request->getStr($this->getSecretKey()), self::PROPERTY_NOTE => $request->getStr(self::PROPERTY_NOTE), ); } protected function processOAuthEditForm( AphrontRequest $request, array $values, $id_error, $secret_error) { $errors = array(); $issues = array(); $key_id = $this->getIDKey(); $key_secret = $this->getSecretKey(); if (!strlen($values[$key_id])) { $errors[] = $id_error; $issues[$key_id] = pht('Required'); } if (!strlen($values[$key_secret])) { $errors[] = $secret_error; $issues[$key_secret] = pht('Required'); } // If the user has not changed the secret, don't update it (that is, // don't cause a bunch of "****" to be written to the database). if (preg_match('/^[*]+$/', $values[$key_secret])) { unset($values[$key_secret]); } return array($errors, $issues, $values); } public function getConfigurationHelp() { $help = $this->getProviderConfigurationHelp(); return $help."\n\n". pht( 'Use the **OAuth App Notes** field to record details about which '. 'account the external application is registered under.'); } abstract protected function getProviderConfigurationHelp(); protected function extendOAuthEditForm( AphrontRequest $request, AphrontFormView $form, array $values, array $issues, $id_label, $secret_label) { $key_id = $this->getIDKey(); $key_secret = $this->getSecretKey(); $key_note = self::PROPERTY_NOTE; $v_id = $values[$key_id]; $v_secret = $values[$key_secret]; if ($v_secret) { $v_secret = str_repeat('*', strlen($v_secret)); } $v_note = $values[$key_note]; $e_id = idx($issues, $key_id, $request->isFormPost() ? null : true); $e_secret = idx($issues, $key_secret, $request->isFormPost() ? null : true); $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel($id_label) ->setName($key_id) ->setValue($v_id) ->setError($e_id)) ->appendChild( id(new AphrontFormPasswordControl()) ->setLabel($secret_label) ->setDisableAutocomplete(true) ->setName($key_secret) ->setValue($v_secret) ->setError($e_secret)) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('OAuth App Notes')) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) ->setName($key_note) ->setValue($v_note)); } public function renderConfigPropertyTransactionTitle( PhabricatorAuthProviderConfigTransaction $xaction) { $author_phid = $xaction->getAuthorPHID(); $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $key = $xaction->getMetadataValue( PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY); switch ($key) { case self::PROPERTY_NOTE: - if (strlen($old)) { + if (phutil_nonempty_string($old)) { return pht( '%s updated the OAuth application notes for this provider.', $xaction->renderHandleLink($author_phid)); } else { return pht( '%s set the OAuth application notes for this provider.', $xaction->renderHandleLink($author_phid)); } } return parent::renderConfigPropertyTransactionTitle($xaction); } protected function willSaveAccount(PhabricatorExternalAccount $account) { parent::willSaveAccount($account); $this->synchronizeOAuthAccount($account); } abstract protected function synchronizeOAuthAccount( PhabricatorExternalAccount $account); }