Path: blob/master/src/applications/auth/provider/PhabricatorOAuth1AuthProvider.php
12256 views
<?php12abstract class PhabricatorOAuth1AuthProvider3extends PhabricatorOAuthAuthProvider {45protected $adapter;67const PROPERTY_CONSUMER_KEY = 'oauth1:consumer:key';8const PROPERTY_CONSUMER_SECRET = 'oauth1:consumer:secret';9const PROPERTY_PRIVATE_KEY = 'oauth1:private:key';1011protected function getIDKey() {12return self::PROPERTY_CONSUMER_KEY;13}1415protected function getSecretKey() {16return self::PROPERTY_CONSUMER_SECRET;17}1819protected function configureAdapter(PhutilOAuth1AuthAdapter $adapter) {20$config = $this->getProviderConfig();21$adapter->setConsumerKey($config->getProperty(self::PROPERTY_CONSUMER_KEY));22$secret = $config->getProperty(self::PROPERTY_CONSUMER_SECRET);23if (phutil_nonempty_string($secret)) {24$adapter->setConsumerSecret(new PhutilOpaqueEnvelope($secret));25}26$adapter->setCallbackURI(PhabricatorEnv::getURI($this->getLoginURI()));27return $adapter;28}2930protected function renderLoginForm(AphrontRequest $request, $mode) {31$attributes = array(32'method' => 'POST',33'uri' => $this->getLoginURI(),34);35return $this->renderStandardLoginButton($request, $mode, $attributes);36}3738public function processLoginRequest(39PhabricatorAuthLoginController $controller) {4041$request = $controller->getRequest();42$adapter = $this->getAdapter();43$account = null;44$response = null;4546if ($request->isHTTPPost()) {47// Add a CSRF code to the callback URI, which we'll verify when48// performing the login.4950$client_code = $this->getAuthCSRFCode($request);5152$callback_uri = $adapter->getCallbackURI();53$callback_uri = $callback_uri.$client_code.'/';54$adapter->setCallbackURI($callback_uri);5556$uri = $adapter->getClientRedirectURI();5758$this->saveHandshakeTokenSecret(59$client_code,60$adapter->getTokenSecret());6162$response = id(new AphrontRedirectResponse())63->setIsExternal(true)64->setURI($uri);65return array($account, $response);66}6768$denied = $request->getStr('denied');69if (strlen($denied)) {70// Twitter indicates that the user cancelled the login attempt by71// returning "denied" as a parameter.72throw new PhutilAuthUserAbortedException();73}7475// NOTE: You can get here via GET, this should probably be a bit more76// user friendly.7778$this->verifyAuthCSRFCode($request, $controller->getExtraURIData());7980$token = $request->getStr('oauth_token');81$verifier = $request->getStr('oauth_verifier');8283if (!$token) {84throw new Exception(pht("Expected '%s' in request!", 'oauth_token'));85}8687if (!$verifier) {88throw new Exception(pht("Expected '%s' in request!", 'oauth_verifier'));89}9091$adapter->setToken($token);92$adapter->setVerifier($verifier);9394$client_code = $this->getAuthCSRFCode($request);95$token_secret = $this->loadHandshakeTokenSecret($client_code);96$adapter->setTokenSecret($token_secret);9798// NOTE: As a side effect, this will cause the OAuth adapter to request99// an access token.100101try {102$identifiers = $adapter->getAccountIdentifiers();103} catch (Exception $ex) {104// TODO: Handle this in a more user-friendly way.105throw $ex;106}107108if (!$identifiers) {109$response = $controller->buildProviderErrorResponse(110$this,111pht(112'The OAuth provider failed to retrieve an account ID.'));113114return array($account, $response);115}116117$account = $this->newExternalAccountForIdentifiers($identifiers);118119return array($account, $response);120}121122public function processEditForm(123AphrontRequest $request,124array $values) {125126$key_ckey = self::PROPERTY_CONSUMER_KEY;127$key_csecret = self::PROPERTY_CONSUMER_SECRET;128129return $this->processOAuthEditForm(130$request,131$values,132pht('Consumer key is required.'),133pht('Consumer secret is required.'));134}135136public function extendEditForm(137AphrontRequest $request,138AphrontFormView $form,139array $values,140array $issues) {141142return $this->extendOAuthEditForm(143$request,144$form,145$values,146$issues,147pht('OAuth Consumer Key'),148pht('OAuth Consumer Secret'));149}150151public function renderConfigPropertyTransactionTitle(152PhabricatorAuthProviderConfigTransaction $xaction) {153154$author_phid = $xaction->getAuthorPHID();155$old = $xaction->getOldValue();156$new = $xaction->getNewValue();157$key = $xaction->getMetadataValue(158PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY);159160switch ($key) {161case self::PROPERTY_CONSUMER_KEY:162if (strlen($old)) {163return pht(164'%s updated the OAuth consumer key for this provider from '.165'"%s" to "%s".',166$xaction->renderHandleLink($author_phid),167$old,168$new);169} else {170return pht(171'%s set the OAuth consumer key for this provider to '.172'"%s".',173$xaction->renderHandleLink($author_phid),174$new);175}176case self::PROPERTY_CONSUMER_SECRET:177if (strlen($old)) {178return pht(179'%s updated the OAuth consumer secret for this provider.',180$xaction->renderHandleLink($author_phid));181} else {182return pht(183'%s set the OAuth consumer secret for this provider.',184$xaction->renderHandleLink($author_phid));185}186}187188return parent::renderConfigPropertyTransactionTitle($xaction);189}190191protected function synchronizeOAuthAccount(192PhabricatorExternalAccount $account) {193$adapter = $this->getAdapter();194195$oauth_token = $adapter->getToken();196$oauth_token_secret = $adapter->getTokenSecret();197198$account->setProperty('oauth1.token', $oauth_token);199$account->setProperty('oauth1.token.secret', $oauth_token_secret);200}201202public function willRenderLinkedAccount(203PhabricatorUser $viewer,204PHUIObjectItemView $item,205PhabricatorExternalAccount $account) {206207$item->addAttribute(pht('OAuth1 Account'));208209parent::willRenderLinkedAccount($viewer, $item, $account);210}211212protected function getContentSecurityPolicyFormActions() {213return $this->getAdapter()->getContentSecurityPolicyFormActions();214}215216/* -( Temporary Secrets )-------------------------------------------------- */217218219private function saveHandshakeTokenSecret($client_code, $secret) {220$secret_type = PhabricatorOAuth1SecretTemporaryTokenType::TOKENTYPE;221$key = $this->getHandshakeTokenKeyFromClientCode($client_code);222$type = $this->getTemporaryTokenType($secret_type);223224// Wipe out an existing token, if one exists.225$token = id(new PhabricatorAuthTemporaryTokenQuery())226->setViewer(PhabricatorUser::getOmnipotentUser())227->withTokenResources(array($key))228->withTokenTypes(array($type))229->executeOne();230if ($token) {231$token->delete();232}233234// Save the new secret.235id(new PhabricatorAuthTemporaryToken())236->setTokenResource($key)237->setTokenType($type)238->setTokenExpires(time() + phutil_units('1 hour in seconds'))239->setTokenCode($secret)240->save();241}242243private function loadHandshakeTokenSecret($client_code) {244$secret_type = PhabricatorOAuth1SecretTemporaryTokenType::TOKENTYPE;245$key = $this->getHandshakeTokenKeyFromClientCode($client_code);246$type = $this->getTemporaryTokenType($secret_type);247248$token = id(new PhabricatorAuthTemporaryTokenQuery())249->setViewer(PhabricatorUser::getOmnipotentUser())250->withTokenResources(array($key))251->withTokenTypes(array($type))252->withExpired(false)253->executeOne();254255if (!$token) {256throw new Exception(257pht(258'Unable to load your OAuth1 token secret from storage. It may '.259'have expired. Try authenticating again.'));260}261262return $token->getTokenCode();263}264265private function getTemporaryTokenType($core_type) {266// Namespace the type so that multiple providers don't step on each267// others' toes if a user starts Mediawiki and Bitbucket auth at the268// same time.269270// TODO: This isn't really a proper use of the table and should get271// cleaned up some day: the type should be constant.272273return $core_type.':'.$this->getProviderConfig()->getID();274}275276private function getHandshakeTokenKeyFromClientCode($client_code) {277// NOTE: This is very slightly coercive since the TemporaryToken table278// expects an "objectPHID" as an identifier, but nothing about the storage279// is bound to PHIDs.280281return 'oauth1:secret/'.$client_code;282}283284}285286287