Summary: Depends on D21018. Ref T13493. Ref T6703. The "ExternalAccount" table has a unique key on `<accountType, accountDomain, accountID>` but this no longer matches our model of reality and changes in this sequence end writes to `accountID`. Remove this key. Then, remove all readers of `accountType` and `accountDomain` (and all nontrivial writers) because none of these callsites are well-aligned with plans in T6703. This change has no user-facing impact today: all the rules about linking/unlinking/etc remain unchanged, because other rules currently prevent creation of more than one provider with a given "accountType". Test Plan: - Linked an OAuth1 account (JIRA). - Linked an OAuth2 account (Asana). - Used `bin/auth refresh` to cycle OAuth tokens. - Grepped for affected symbols. - Published an Asana update. - Published a JIRA link. Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13493, T6703 Differential Revision: https://secure.phabricator.com/D21019
		
			
				
	
	
		
			292 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			292 lines
		
	
	
		
			8.1 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
 | 
						|
abstract class PhabricatorOAuth2AuthProvider
 | 
						|
  extends PhabricatorOAuthAuthProvider {
 | 
						|
 | 
						|
  const PROPERTY_APP_ID = 'oauth:app:id';
 | 
						|
  const PROPERTY_APP_SECRET = 'oauth:app:secret';
 | 
						|
 | 
						|
  protected function getIDKey() {
 | 
						|
    return self::PROPERTY_APP_ID;
 | 
						|
  }
 | 
						|
 | 
						|
  protected function getSecretKey() {
 | 
						|
    return self::PROPERTY_APP_SECRET;
 | 
						|
  }
 | 
						|
 | 
						|
 | 
						|
  protected function configureAdapter(PhutilOAuthAuthAdapter $adapter) {
 | 
						|
    $config = $this->getProviderConfig();
 | 
						|
    $adapter->setClientID($config->getProperty(self::PROPERTY_APP_ID));
 | 
						|
    $adapter->setClientSecret(
 | 
						|
      new PhutilOpaqueEnvelope(
 | 
						|
        $config->getProperty(self::PROPERTY_APP_SECRET)));
 | 
						|
    $adapter->setRedirectURI(PhabricatorEnv::getURI($this->getLoginURI()));
 | 
						|
    return $adapter;
 | 
						|
  }
 | 
						|
 | 
						|
  protected function renderLoginForm(AphrontRequest $request, $mode) {
 | 
						|
    $adapter = $this->getAdapter();
 | 
						|
    $adapter->setState($this->getAuthCSRFCode($request));
 | 
						|
 | 
						|
    $scope = $request->getStr('scope');
 | 
						|
    if ($scope) {
 | 
						|
      $adapter->setScope($scope);
 | 
						|
    }
 | 
						|
 | 
						|
    $attributes = array(
 | 
						|
      'method' => 'GET',
 | 
						|
      'uri' => $adapter->getAuthenticateURI(),
 | 
						|
    );
 | 
						|
 | 
						|
    return $this->renderStandardLoginButton($request, $mode, $attributes);
 | 
						|
  }
 | 
						|
 | 
						|
  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);
 | 
						|
    }
 | 
						|
 | 
						|
    $this->verifyAuthCSRFCode($request, $request->getStr('state'));
 | 
						|
 | 
						|
    $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 {
 | 
						|
      $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) {
 | 
						|
 | 
						|
    return $this->processOAuthEditForm(
 | 
						|
      $request,
 | 
						|
      $values,
 | 
						|
      pht('Application ID is required.'),
 | 
						|
      pht('Application secret is required.'));
 | 
						|
  }
 | 
						|
 | 
						|
  public function extendEditForm(
 | 
						|
    AphrontRequest $request,
 | 
						|
    AphrontFormView $form,
 | 
						|
    array $values,
 | 
						|
    array $issues) {
 | 
						|
 | 
						|
    return $this->extendOAuthEditForm(
 | 
						|
      $request,
 | 
						|
      $form,
 | 
						|
      $values,
 | 
						|
      $issues,
 | 
						|
      pht('OAuth App ID'),
 | 
						|
      pht('OAuth App 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_APP_ID:
 | 
						|
        if (strlen($old)) {
 | 
						|
          return pht(
 | 
						|
            '%s updated the OAuth application ID for this provider from '.
 | 
						|
            '"%s" to "%s".',
 | 
						|
            $xaction->renderHandleLink($author_phid),
 | 
						|
            $old,
 | 
						|
            $new);
 | 
						|
        } else {
 | 
						|
          return pht(
 | 
						|
            '%s set the OAuth application ID for this provider to '.
 | 
						|
            '"%s".',
 | 
						|
            $xaction->renderHandleLink($author_phid),
 | 
						|
            $new);
 | 
						|
        }
 | 
						|
      case self::PROPERTY_APP_SECRET:
 | 
						|
        if (strlen($old)) {
 | 
						|
          return pht(
 | 
						|
            '%s updated the OAuth application secret for this provider.',
 | 
						|
            $xaction->renderHandleLink($author_phid));
 | 
						|
        } else {
 | 
						|
          return pht(
 | 
						|
            '%s set the OAuth application secret for this provider.',
 | 
						|
            $xaction->renderHandleLink($author_phid));
 | 
						|
        }
 | 
						|
      case self::PROPERTY_NOTE:
 | 
						|
        if (strlen($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 synchronizeOAuthAccount(
 | 
						|
    PhabricatorExternalAccount $account) {
 | 
						|
    $adapter = $this->getAdapter();
 | 
						|
 | 
						|
    $oauth_token = $adapter->getAccessToken();
 | 
						|
    $account->setProperty('oauth.token.access', $oauth_token);
 | 
						|
 | 
						|
    if ($adapter->supportsTokenRefresh()) {
 | 
						|
      $refresh_token = $adapter->getRefreshToken();
 | 
						|
      $account->setProperty('oauth.token.refresh', $refresh_token);
 | 
						|
    } else {
 | 
						|
      $account->setProperty('oauth.token.refresh', null);
 | 
						|
    }
 | 
						|
 | 
						|
    $expires = $adapter->getAccessTokenExpires();
 | 
						|
    $account->setProperty('oauth.token.access.expires', $expires);
 | 
						|
  }
 | 
						|
 | 
						|
  public function getOAuthAccessToken(
 | 
						|
    PhabricatorExternalAccount $account,
 | 
						|
    $force_refresh = false) {
 | 
						|
 | 
						|
    if ($account->getProviderConfigPHID() !== $this->getProviderConfigPHID()) {
 | 
						|
      throw new Exception(pht('Account does not match provider!'));
 | 
						|
    }
 | 
						|
 | 
						|
    if (!$force_refresh) {
 | 
						|
      $access_expires = $account->getProperty('oauth.token.access.expires');
 | 
						|
      $access_token = $account->getProperty('oauth.token.access');
 | 
						|
 | 
						|
      // Don't return a token with fewer than this many seconds remaining until
 | 
						|
      // it expires.
 | 
						|
      $shortest_token = 60;
 | 
						|
      if ($access_token) {
 | 
						|
        if ($access_expires === null ||
 | 
						|
            $access_expires > (time() + $shortest_token)) {
 | 
						|
          return $access_token;
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    $refresh_token = $account->getProperty('oauth.token.refresh');
 | 
						|
    if ($refresh_token) {
 | 
						|
      $adapter = $this->getAdapter();
 | 
						|
      if ($adapter->supportsTokenRefresh()) {
 | 
						|
        $adapter->refreshAccessToken($refresh_token);
 | 
						|
 | 
						|
        $this->synchronizeOAuthAccount($account);
 | 
						|
        $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
 | 
						|
          $account->save();
 | 
						|
        unset($unguarded);
 | 
						|
 | 
						|
        return $account->getProperty('oauth.token.access');
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  public function willRenderLinkedAccount(
 | 
						|
    PhabricatorUser $viewer,
 | 
						|
    PHUIObjectItemView $item,
 | 
						|
    PhabricatorExternalAccount $account) {
 | 
						|
 | 
						|
    // Get a valid token, possibly refreshing it. If we're unable to refresh
 | 
						|
    // it, render a message to that effect. The user may be able to repair the
 | 
						|
    // link by manually reconnecting.
 | 
						|
 | 
						|
    $is_invalid = false;
 | 
						|
    try {
 | 
						|
      $oauth_token = $this->getOAuthAccessToken($account);
 | 
						|
    } catch (Exception $ex) {
 | 
						|
      $oauth_token = null;
 | 
						|
      $is_invalid = true;
 | 
						|
    }
 | 
						|
 | 
						|
    $item->addAttribute(pht('OAuth2 Account'));
 | 
						|
 | 
						|
    if ($oauth_token) {
 | 
						|
      $oauth_expires = $account->getProperty('oauth.token.access.expires');
 | 
						|
      if ($oauth_expires) {
 | 
						|
        $item->addAttribute(
 | 
						|
          pht(
 | 
						|
            'Active OAuth Token (Expires: %s)',
 | 
						|
            phabricator_datetime($oauth_expires, $viewer)));
 | 
						|
      } else {
 | 
						|
        $item->addAttribute(
 | 
						|
          pht('Active OAuth Token'));
 | 
						|
      }
 | 
						|
    } else if ($is_invalid) {
 | 
						|
      $item->addAttribute(pht('Invalid OAuth Access Token'));
 | 
						|
    } else {
 | 
						|
      $item->addAttribute(pht('No OAuth Access Token'));
 | 
						|
    }
 | 
						|
 | 
						|
    parent::willRenderLinkedAccount($viewer, $item, $account);
 | 
						|
  }
 | 
						|
 | 
						|
  public function supportsAutoLogin() {
 | 
						|
    return true;
 | 
						|
  }
 | 
						|
 | 
						|
  public function getAutoLoginURI(AphrontRequest $request) {
 | 
						|
    $csrf_code = $this->getAuthCSRFCode($request);
 | 
						|
 | 
						|
    $adapter = $this->getAdapter();
 | 
						|
    $adapter->setState($csrf_code);
 | 
						|
 | 
						|
    return $adapter->getAuthenticateURI();
 | 
						|
  }
 | 
						|
 | 
						|
}
 |