@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

OAuth - add a little notes section for admins to remember details about external accounts

Summary: Fixes T4755. This also includes putting in a note that Google might ToS you to use the Google+ API. Lots of code here as there was some repeated stuff between OAuth1 and OAuth2 so I made a base OAuth with less-base OAuth1 and OAuth2 inheriting from it. The JIRA provider remains an independent mess and didn't get the notes field thing.

Test Plan: looked at providers and read pretty instructions.

Reviewers: epriestley

Reviewed By: epriestley

Subscribers: epriestley, Korvin

Maniphest Tasks: T4755

Differential Revision: https://secure.phabricator.com/D8726

+370 -321
+10 -8
src/__phutil_library_map__.php
··· 1228 1228 'PhabricatorAuthProviderOAuth1' => 'applications/auth/provider/PhabricatorAuthProviderOAuth1.php', 1229 1229 'PhabricatorAuthProviderOAuth1JIRA' => 'applications/auth/provider/PhabricatorAuthProviderOAuth1JIRA.php', 1230 1230 'PhabricatorAuthProviderOAuth1Twitter' => 'applications/auth/provider/PhabricatorAuthProviderOAuth1Twitter.php', 1231 + 'PhabricatorAuthProviderOAuth2' => 'applications/auth/provider/PhabricatorAuthProviderOAuth2.php', 1231 1232 'PhabricatorAuthProviderOAuthAmazon' => 'applications/auth/provider/PhabricatorAuthProviderOAuthAmazon.php', 1232 1233 'PhabricatorAuthProviderOAuthAsana' => 'applications/auth/provider/PhabricatorAuthProviderOAuthAsana.php', 1233 1234 'PhabricatorAuthProviderOAuthDisqus' => 'applications/auth/provider/PhabricatorAuthProviderOAuthDisqus.php', ··· 3955 3956 'PhabricatorAuthProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 3956 3957 'PhabricatorAuthProviderLDAP' => 'PhabricatorAuthProvider', 3957 3958 'PhabricatorAuthProviderOAuth' => 'PhabricatorAuthProvider', 3958 - 'PhabricatorAuthProviderOAuth1' => 'PhabricatorAuthProvider', 3959 + 'PhabricatorAuthProviderOAuth1' => 'PhabricatorAuthProviderOAuth', 3959 3960 'PhabricatorAuthProviderOAuth1JIRA' => 'PhabricatorAuthProviderOAuth1', 3960 3961 'PhabricatorAuthProviderOAuth1Twitter' => 'PhabricatorAuthProviderOAuth1', 3961 - 'PhabricatorAuthProviderOAuthAmazon' => 'PhabricatorAuthProviderOAuth', 3962 - 'PhabricatorAuthProviderOAuthAsana' => 'PhabricatorAuthProviderOAuth', 3963 - 'PhabricatorAuthProviderOAuthDisqus' => 'PhabricatorAuthProviderOAuth', 3964 - 'PhabricatorAuthProviderOAuthFacebook' => 'PhabricatorAuthProviderOAuth', 3965 - 'PhabricatorAuthProviderOAuthGitHub' => 'PhabricatorAuthProviderOAuth', 3966 - 'PhabricatorAuthProviderOAuthGoogle' => 'PhabricatorAuthProviderOAuth', 3967 - 'PhabricatorAuthProviderOAuthTwitch' => 'PhabricatorAuthProviderOAuth', 3962 + 'PhabricatorAuthProviderOAuth2' => 'PhabricatorAuthProviderOAuth', 3963 + 'PhabricatorAuthProviderOAuthAmazon' => 'PhabricatorAuthProviderOAuth2', 3964 + 'PhabricatorAuthProviderOAuthAsana' => 'PhabricatorAuthProviderOAuth2', 3965 + 'PhabricatorAuthProviderOAuthDisqus' => 'PhabricatorAuthProviderOAuth2', 3966 + 'PhabricatorAuthProviderOAuthFacebook' => 'PhabricatorAuthProviderOAuth2', 3967 + 'PhabricatorAuthProviderOAuthGitHub' => 'PhabricatorAuthProviderOAuth2', 3968 + 'PhabricatorAuthProviderOAuthGoogle' => 'PhabricatorAuthProviderOAuth2', 3969 + 'PhabricatorAuthProviderOAuthTwitch' => 'PhabricatorAuthProviderOAuth2', 3968 3970 'PhabricatorAuthProviderPassword' => 'PhabricatorAuthProvider', 3969 3971 'PhabricatorAuthProviderPersona' => 'PhabricatorAuthProvider', 3970 3972 'PhabricatorAuthRegisterController' => 'PhabricatorAuthController',
+55 -212
src/applications/auth/provider/PhabricatorAuthProviderOAuth.php
··· 2 2 3 3 abstract class PhabricatorAuthProviderOAuth extends PhabricatorAuthProvider { 4 4 5 + const PROPERTY_NOTE = 'oauth:app:note'; 6 + 5 7 protected $adapter; 6 8 7 9 abstract protected function newOAuthAdapter(); 10 + abstract protected function getIDKey(); 11 + abstract protected function getSecretKey(); 8 12 9 13 public function getDescriptionForCreate() { 10 14 return pht('Configure %s OAuth.', $this->getProviderName()); ··· 19 23 return $this->adapter; 20 24 } 21 25 22 - protected function configureAdapter(PhutilAuthAdapterOAuth $adapter) { 23 - $config = $this->getProviderConfig(); 24 - $adapter->setClientID($config->getProperty(self::PROPERTY_APP_ID)); 25 - $adapter->setClientSecret( 26 - new PhutilOpaqueEnvelope( 27 - $config->getProperty(self::PROPERTY_APP_SECRET))); 28 - $adapter->setRedirectURI(PhabricatorEnv::getURI($this->getLoginURI())); 29 - return $adapter; 30 - } 31 - 32 26 public function isLoginFormAButton() { 33 27 return true; 34 28 } 35 29 36 - protected function renderLoginForm(AphrontRequest $request, $mode) { 37 - $adapter = $this->getAdapter(); 38 - $adapter->setState($this->getAuthCSRFCode($request)); 39 - 40 - $scope = $request->getStr('scope'); 41 - if ($scope) { 42 - $adapter->setScope($scope); 43 - } 44 - 45 - $attributes = array( 46 - 'method' => 'GET', 47 - 'uri' => $adapter->getAuthenticateURI(), 48 - ); 49 - 50 - return $this->renderStandardLoginButton($request, $mode, $attributes); 51 - } 52 - 53 - public function processLoginRequest( 54 - PhabricatorAuthLoginController $controller) { 55 - 56 - $request = $controller->getRequest(); 57 - $adapter = $this->getAdapter(); 58 - $account = null; 59 - $response = null; 60 - 61 - $error = $request->getStr('error'); 62 - if ($error) { 63 - $response = $controller->buildProviderErrorResponse( 64 - $this, 65 - pht( 66 - 'The OAuth provider returned an error: %s', 67 - $error)); 68 - 69 - return array($account, $response); 70 - } 71 - 72 - $this->verifyAuthCSRFCode($request, $request->getStr('state')); 73 - 74 - $code = $request->getStr('code'); 75 - if (!strlen($code)) { 76 - $response = $controller->buildProviderErrorResponse( 77 - $this, 78 - pht( 79 - 'The OAuth provider did not return a "code" parameter in its '. 80 - 'response.')); 81 - 82 - return array($account, $response); 83 - } 84 - 85 - $adapter->setCode($code); 86 - 87 - // NOTE: As a side effect, this will cause the OAuth adapter to request 88 - // an access token. 89 - 90 - try { 91 - $account_id = $adapter->getAccountID(); 92 - } catch (Exception $ex) { 93 - // TODO: Handle this in a more user-friendly way. 94 - throw $ex; 95 - } 96 - 97 - if (!strlen($account_id)) { 98 - $response = $controller->buildProviderErrorResponse( 99 - $this, 100 - pht( 101 - 'The OAuth provider failed to retrieve an account ID.')); 102 - 103 - return array($account, $response); 104 - } 105 - 106 - return array($this->loadOrCreateAccount($account_id), $response); 107 - } 108 - 109 - const PROPERTY_APP_ID = 'oauth:app:id'; 110 - const PROPERTY_APP_SECRET = 'oauth:app:secret'; 111 - 112 30 public function readFormValuesFromProvider() { 113 31 $config = $this->getProviderConfig(); 114 - $id = $config->getProperty(self::PROPERTY_APP_ID); 115 - $secret = $config->getProperty(self::PROPERTY_APP_SECRET); 32 + $id = $config->getProperty($this->getIDKey()); 33 + $secret = $config->getProperty($this->getSecretKey()); 34 + $note = $config->getProperty(self::PROPERTY_NOTE); 116 35 117 36 return array( 118 - self::PROPERTY_APP_ID => $id, 119 - self::PROPERTY_APP_SECRET => $secret, 37 + $this->getIDKey() => $id, 38 + $this->getSecretKey() => $secret, 39 + self::PROPERTY_NOTE => $note, 120 40 ); 121 41 } 122 42 123 43 public function readFormValuesFromRequest(AphrontRequest $request) { 124 44 return array( 125 - self::PROPERTY_APP_ID => $request->getStr(self::PROPERTY_APP_ID), 126 - self::PROPERTY_APP_SECRET => $request->getStr(self::PROPERTY_APP_SECRET), 45 + $this->getIDKey() => $request->getStr($this->getIDKey()), 46 + $this->getSecretKey() => $request->getStr($this->getSecretKey()), 47 + self::PROPERTY_NOTE => $request->getStr(self::PROPERTY_NOTE), 127 48 ); 128 49 } 129 50 130 - public function processEditForm( 51 + protected function processOAuthEditForm( 131 52 AphrontRequest $request, 132 - array $values) { 53 + array $values, 54 + $id_error, 55 + $secret_error) { 56 + 133 57 $errors = array(); 134 58 $issues = array(); 135 - 136 - $key_id = self::PROPERTY_APP_ID; 137 - $key_secret = self::PROPERTY_APP_SECRET; 59 + $key_id = $this->getIDKey(); 60 + $key_secret = $this->getSecretKey(); 138 61 139 62 if (!strlen($values[$key_id])) { 140 - $errors[] = pht('Application ID is required.'); 63 + $errors[] = $id_error; 141 64 $issues[$key_id] = pht('Required'); 142 65 } 143 66 144 67 if (!strlen($values[$key_secret])) { 145 - $errors[] = pht('Application secret is required.'); 68 + $errors[] = $secret_error; 146 69 $issues[$key_secret] = pht('Required'); 147 70 } 148 71 ··· 155 78 return array($errors, $issues, $values); 156 79 } 157 80 158 - public function extendEditForm( 81 + public function getConfigurationHelp() { 82 + $help = $this->getProviderConfigurationHelp(); 83 + 84 + return $help . "\n\n" . 85 + pht('Use the **OAuth App Notes** field to record details about which '. 86 + 'account the external application is registered under.'); 87 + } 88 + 89 + abstract protected function getProviderConfigurationHelp(); 90 + 91 + protected function extendOAuthEditForm( 159 92 AphrontRequest $request, 160 93 AphrontFormView $form, 161 94 array $values, 162 - array $issues) { 95 + array $issues, 96 + $id_label, 97 + $secret_label) { 163 98 164 - $key_id = self::PROPERTY_APP_ID; 165 - $key_secret = self::PROPERTY_APP_SECRET; 99 + $key_id = $this->getIDKey(); 100 + $key_secret = $this->getSecretKey(); 101 + $key_note = self::PROPERTY_NOTE; 166 102 167 103 $v_id = $values[$key_id]; 168 104 $v_secret = $values[$key_secret]; 169 105 if ($v_secret) { 170 106 $v_secret = str_repeat('*', strlen($v_secret)); 171 107 } 108 + $v_note = $values[$key_note]; 172 109 173 110 $e_id = idx($issues, $key_id, $request->isFormPost() ? null : true); 174 111 $e_secret = idx($issues, $key_secret, $request->isFormPost() ? null : true); ··· 176 113 $form 177 114 ->appendChild( 178 115 id(new AphrontFormTextControl()) 179 - ->setLabel(pht('OAuth App ID')) 116 + ->setLabel($id_label) 180 117 ->setName($key_id) 181 118 ->setValue($v_id) 182 119 ->setError($e_id)) 183 120 ->appendChild( 184 121 id(new AphrontFormPasswordControl()) 185 - ->setLabel(pht('OAuth App Secret')) 122 + ->setLabel($secret_label) 186 123 ->setName($key_secret) 187 124 ->setValue($v_secret) 188 - ->setError($e_secret)); 125 + ->setError($e_secret)) 126 + ->appendChild( 127 + id(new AphrontFormTextAreaControl()) 128 + ->setLabel(pht('OAuth App Notes')) 129 + ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) 130 + ->setName($key_note) 131 + ->setValue($v_note)); 189 132 } 190 133 191 134 public function renderConfigPropertyTransactionTitle( ··· 198 141 PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY); 199 142 200 143 switch ($key) { 201 - case self::PROPERTY_APP_ID: 144 + case self::PROPERTY_NOTE: 202 145 if (strlen($old)) { 203 146 return pht( 204 - '%s updated the OAuth application ID for this provider from '. 205 - '"%s" to "%s".', 206 - $xaction->renderHandleLink($author_phid), 207 - $old, 208 - $new); 209 - } else { 210 - return pht( 211 - '%s set the OAuth application ID for this provider to '. 212 - '"%s".', 213 - $xaction->renderHandleLink($author_phid), 214 - $new); 215 - } 216 - case self::PROPERTY_APP_SECRET: 217 - if (strlen($old)) { 218 - return pht( 219 - '%s updated the OAuth application secret for this provider.', 147 + '%s updated the OAuth application notes for this provider.', 220 148 $xaction->renderHandleLink($author_phid)); 221 149 } else { 222 150 return pht( 223 - '%s set the OAuth application seceret for this provider.', 151 + '%s set the OAuth application notes for this provider.', 224 152 $xaction->renderHandleLink($author_phid)); 225 153 } 154 + 226 155 } 227 156 228 157 return parent::renderConfigPropertyTransactionTitle($xaction); ··· 233 162 $this->synchronizeOAuthAccount($account); 234 163 } 235 164 236 - protected function synchronizeOAuthAccount( 237 - PhabricatorExternalAccount $account) { 238 - $adapter = $this->getAdapter(); 239 - 240 - $oauth_token = $adapter->getAccessToken(); 241 - $account->setProperty('oauth.token.access', $oauth_token); 242 - 243 - if ($adapter->supportsTokenRefresh()) { 244 - $refresh_token = $adapter->getRefreshToken(); 245 - $account->setProperty('oauth.token.refresh', $refresh_token); 246 - } else { 247 - $account->setProperty('oauth.token.refresh', null); 248 - } 249 - 250 - $expires = $adapter->getAccessTokenExpires(); 251 - $account->setProperty('oauth.token.access.expires', $expires); 252 - } 253 - 254 - public function getOAuthAccessToken( 255 - PhabricatorExternalAccount $account, 256 - $force_refresh = false) { 257 - 258 - if ($account->getProviderKey() !== $this->getProviderKey()) { 259 - throw new Exception("Account does not match provider!"); 260 - } 261 - 262 - if (!$force_refresh) { 263 - $access_expires = $account->getProperty('oauth.token.access.expires'); 264 - $access_token = $account->getProperty('oauth.token.access'); 265 - 266 - // Don't return a token with fewer than this many seconds remaining until 267 - // it expires. 268 - $shortest_token = 60; 269 - if ($access_token) { 270 - if ($access_expires === null || 271 - $access_expires > (time() + $shortest_token)) { 272 - return $access_token; 273 - } 274 - } 275 - } 276 - 277 - $refresh_token = $account->getProperty('oauth.token.refresh'); 278 - if ($refresh_token) { 279 - $adapter = $this->getAdapter(); 280 - if ($adapter->supportsTokenRefresh()) { 281 - $adapter->refreshAccessToken($refresh_token); 282 - 283 - $this->synchronizeOAuthAccount($account); 284 - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 285 - $account->save(); 286 - unset($unguarded); 287 - 288 - return $account->getProperty('oauth.token.access'); 289 - } 290 - } 291 - 292 - return null; 293 - } 294 - 295 - public function willRenderLinkedAccount( 296 - PhabricatorUser $viewer, 297 - PHUIObjectItemView $item, 298 - PhabricatorExternalAccount $account) { 299 - 300 - // Get a valid token, possibly refreshing it. 301 - $oauth_token = $this->getOAuthAccessToken($account); 302 - 303 - $item->addAttribute(pht('OAuth2 Account')); 304 - 305 - if ($oauth_token) { 306 - $oauth_expires = $account->getProperty('oauth.token.access.expires'); 307 - if ($oauth_expires) { 308 - $item->addAttribute( 309 - pht( 310 - 'Active OAuth Token (Expires: %s)', 311 - phabricator_datetime($oauth_expires, $viewer))); 312 - } else { 313 - $item->addAttribute( 314 - pht( 315 - 'Active OAuth Token')); 316 - } 317 - } else { 318 - $item->addAttribute(pht('No OAuth Access Token')); 319 - } 320 - 321 - parent::willRenderLinkedAccount($viewer, $item, $account); 322 - } 323 - 165 + abstract protected function synchronizeOAuthAccount( 166 + PhabricatorExternalAccount $account); 324 167 325 168 }
+18 -86
src/applications/auth/provider/PhabricatorAuthProviderOAuth1.php
··· 1 1 <?php 2 2 3 - abstract class PhabricatorAuthProviderOAuth1 extends PhabricatorAuthProvider { 3 + abstract class PhabricatorAuthProviderOAuth1 4 + extends PhabricatorAuthProviderOAuth { 4 5 5 6 protected $adapter; 6 7 ··· 8 9 const PROPERTY_CONSUMER_SECRET = 'oauth1:consumer:secret'; 9 10 const PROPERTY_PRIVATE_KEY = 'oauth1:private:key'; 10 11 11 - abstract protected function newOAuthAdapter(); 12 - 13 - public function getDescriptionForCreate() { 14 - return pht('Configure %s OAuth.', $this->getProviderName()); 12 + protected function getIDKey() { 13 + return self::PROPERTY_CONSUMER_KEY; 15 14 } 16 15 17 - public function getAdapter() { 18 - if (!$this->adapter) { 19 - $adapter = $this->newOAuthAdapter(); 20 - $this->adapter = $adapter; 21 - $this->configureAdapter($adapter); 22 - } 23 - return $this->adapter; 16 + protected function getSecretKey() { 17 + return self::PROPERTY_CONSUMER_SECRET; 24 18 } 25 19 26 20 protected function configureAdapter(PhutilAuthAdapterOAuth1 $adapter) { ··· 32 26 } 33 27 $adapter->setCallbackURI(PhabricatorEnv::getURI($this->getLoginURI())); 34 28 return $adapter; 35 - } 36 - 37 - public function isLoginFormAButton() { 38 - return true; 39 29 } 40 30 41 31 protected function renderLoginForm(AphrontRequest $request, $mode) { ··· 117 107 return array($this->loadOrCreateAccount($account_id), $response); 118 108 } 119 109 120 - public function readFormValuesFromProvider() { 121 - $config = $this->getProviderConfig(); 122 - $id = $config->getProperty(self::PROPERTY_CONSUMER_KEY); 123 - $secret = $config->getProperty(self::PROPERTY_CONSUMER_SECRET); 124 - 125 - return array( 126 - self::PROPERTY_CONSUMER_KEY => $id, 127 - self::PROPERTY_CONSUMER_SECRET => $secret, 128 - ); 129 - } 130 - 131 - public function readFormValuesFromRequest(AphrontRequest $request) { 132 - return array( 133 - self::PROPERTY_CONSUMER_KEY 134 - => $request->getStr(self::PROPERTY_CONSUMER_KEY), 135 - self::PROPERTY_CONSUMER_SECRET 136 - => $request->getStr(self::PROPERTY_CONSUMER_SECRET), 137 - ); 138 - } 139 - 140 110 public function processEditForm( 141 111 AphrontRequest $request, 142 112 array $values) { 143 - $errors = array(); 144 - $issues = array(); 145 113 146 114 $key_ckey = self::PROPERTY_CONSUMER_KEY; 147 115 $key_csecret = self::PROPERTY_CONSUMER_SECRET; 148 116 149 - if (!strlen($values[$key_ckey])) { 150 - $errors[] = pht('Consumer key is required.'); 151 - $issues[$key_ckey] = pht('Required'); 152 - } 153 - 154 - if (!strlen($values[$key_csecret])) { 155 - $errors[] = pht('Consumer secret is required.'); 156 - $issues[$key_csecret] = pht('Required'); 157 - } 158 - 159 - // If the user has not changed the secret, don't update it (that is, 160 - // don't cause a bunch of "****" to be written to the database). 161 - if (preg_match('/^[*]+$/', $values[$key_csecret])) { 162 - unset($values[$key_csecret]); 163 - } 164 - 165 - return array($errors, $issues, $values); 117 + return $this->processOAuthEditForm( 118 + $request, 119 + $values, 120 + pht('Consumer key is required.'), 121 + pht('Consumer secret is required.')); 166 122 } 167 123 168 124 public function extendEditForm( ··· 171 127 array $values, 172 128 array $issues) { 173 129 174 - $key_id = self::PROPERTY_CONSUMER_KEY; 175 - $key_secret = self::PROPERTY_CONSUMER_SECRET; 176 - 177 - $v_id = $values[$key_id]; 178 - $v_secret = $values[$key_secret]; 179 - if ($v_secret) { 180 - $v_secret = str_repeat('*', strlen($v_secret)); 181 - } 182 - 183 - $e_id = idx($issues, $key_id, $request->isFormPost() ? null : true); 184 - $e_secret = idx($issues, $key_secret, $request->isFormPost() ? null : true); 185 - 186 - $form 187 - ->appendChild( 188 - id(new AphrontFormTextControl()) 189 - ->setLabel(pht('OAuth Consumer Key')) 190 - ->setName($key_id) 191 - ->setValue($v_id) 192 - ->setError($e_id)) 193 - ->appendChild( 194 - id(new AphrontFormPasswordControl()) 195 - ->setLabel(pht('OAuth Consumer Secret')) 196 - ->setName($key_secret) 197 - ->setValue($v_secret) 198 - ->setError($e_secret)); 130 + return $this->extendOAuthEditForm( 131 + $request, 132 + $form, 133 + $values, 134 + $issues, 135 + pht('OAuth Consumer Key'), 136 + pht('OAuth Consumer Secret')); 199 137 } 200 138 201 139 public function renderConfigPropertyTransactionTitle( ··· 238 176 return parent::renderConfigPropertyTransactionTitle($xaction); 239 177 } 240 178 241 - protected function willSaveAccount(PhabricatorExternalAccount $account) { 242 - parent::willSaveAccount($account); 243 - $this->synchronizeOAuthAccount($account); 244 - } 245 - 246 179 protected function synchronizeOAuthAccount( 247 180 PhabricatorExternalAccount $account) { 248 181 $adapter = $this->getAdapter(); ··· 263 196 264 197 parent::willRenderLinkedAccount($viewer, $item, $account); 265 198 } 266 - 267 199 268 200 }
+4
src/applications/auth/provider/PhabricatorAuthProviderOAuth1JIRA.php
··· 16 16 } 17 17 18 18 public function getConfigurationHelp() { 19 + return $this->getProviderConfigurationHelp(); 20 + } 21 + 22 + protected function getProviderConfigurationHelp() { 19 23 if ($this->isSetup()) { 20 24 return pht( 21 25 "**Step 1 of 2**: Provide the name and URI for your JIRA install.\n\n".
+1 -1
src/applications/auth/provider/PhabricatorAuthProviderOAuth1Twitter.php
··· 7 7 return pht('Twitter'); 8 8 } 9 9 10 - public function getConfigurationHelp() { 10 + protected function getProviderConfigurationHelp() { 11 11 $login_uri = PhabricatorEnv::getURI($this->getLoginURI()); 12 12 13 13 return pht(
+266
src/applications/auth/provider/PhabricatorAuthProviderOAuth2.php
··· 1 + <?php 2 + 3 + abstract class PhabricatorAuthProviderOAuth2 4 + extends PhabricatorAuthProviderOAuth { 5 + 6 + const PROPERTY_APP_ID = 'oauth:app:id'; 7 + const PROPERTY_APP_SECRET = 'oauth:app:secret'; 8 + 9 + protected function getIDKey() { 10 + return self::PROPERTY_APP_ID; 11 + } 12 + 13 + protected function getSecretKey() { 14 + return self::PROPERTY_APP_SECRET; 15 + } 16 + 17 + 18 + protected function configureAdapter(PhutilAuthAdapterOAuth $adapter) { 19 + $config = $this->getProviderConfig(); 20 + $adapter->setClientID($config->getProperty(self::PROPERTY_APP_ID)); 21 + $adapter->setClientSecret( 22 + new PhutilOpaqueEnvelope( 23 + $config->getProperty(self::PROPERTY_APP_SECRET))); 24 + $adapter->setRedirectURI(PhabricatorEnv::getURI($this->getLoginURI())); 25 + return $adapter; 26 + } 27 + 28 + protected function renderLoginForm(AphrontRequest $request, $mode) { 29 + $adapter = $this->getAdapter(); 30 + $adapter->setState($this->getAuthCSRFCode($request)); 31 + 32 + $scope = $request->getStr('scope'); 33 + if ($scope) { 34 + $adapter->setScope($scope); 35 + } 36 + 37 + $attributes = array( 38 + 'method' => 'GET', 39 + 'uri' => $adapter->getAuthenticateURI(), 40 + ); 41 + 42 + return $this->renderStandardLoginButton($request, $mode, $attributes); 43 + } 44 + 45 + public function processLoginRequest( 46 + PhabricatorAuthLoginController $controller) { 47 + 48 + $request = $controller->getRequest(); 49 + $adapter = $this->getAdapter(); 50 + $account = null; 51 + $response = null; 52 + 53 + $error = $request->getStr('error'); 54 + if ($error) { 55 + $response = $controller->buildProviderErrorResponse( 56 + $this, 57 + pht( 58 + 'The OAuth provider returned an error: %s', 59 + $error)); 60 + 61 + return array($account, $response); 62 + } 63 + 64 + $this->verifyAuthCSRFCode($request, $request->getStr('state')); 65 + 66 + $code = $request->getStr('code'); 67 + if (!strlen($code)) { 68 + $response = $controller->buildProviderErrorResponse( 69 + $this, 70 + pht( 71 + 'The OAuth provider did not return a "code" parameter in its '. 72 + 'response.')); 73 + 74 + return array($account, $response); 75 + } 76 + 77 + $adapter->setCode($code); 78 + 79 + // NOTE: As a side effect, this will cause the OAuth adapter to request 80 + // an access token. 81 + 82 + try { 83 + $account_id = $adapter->getAccountID(); 84 + } catch (Exception $ex) { 85 + // TODO: Handle this in a more user-friendly way. 86 + throw $ex; 87 + } 88 + 89 + if (!strlen($account_id)) { 90 + $response = $controller->buildProviderErrorResponse( 91 + $this, 92 + pht( 93 + 'The OAuth provider failed to retrieve an account ID.')); 94 + 95 + return array($account, $response); 96 + } 97 + 98 + return array($this->loadOrCreateAccount($account_id), $response); 99 + } 100 + 101 + public function processEditForm( 102 + AphrontRequest $request, 103 + array $values) { 104 + 105 + return $this->processOAuthEditForm( 106 + $request, 107 + $values, 108 + pht('Application ID is required.'), 109 + pht('Application secret is required.')); 110 + } 111 + 112 + public function extendEditForm( 113 + AphrontRequest $request, 114 + AphrontFormView $form, 115 + array $values, 116 + array $issues) { 117 + 118 + return $this->extendOAuthEditForm( 119 + $request, 120 + $form, 121 + $values, 122 + $issues, 123 + pht('OAuth App ID'), 124 + pht('OAuth App Secret')); 125 + } 126 + 127 + public function renderConfigPropertyTransactionTitle( 128 + PhabricatorAuthProviderConfigTransaction $xaction) { 129 + 130 + $author_phid = $xaction->getAuthorPHID(); 131 + $old = $xaction->getOldValue(); 132 + $new = $xaction->getNewValue(); 133 + $key = $xaction->getMetadataValue( 134 + PhabricatorAuthProviderConfigTransaction::PROPERTY_KEY); 135 + 136 + switch ($key) { 137 + case self::PROPERTY_APP_ID: 138 + if (strlen($old)) { 139 + return pht( 140 + '%s updated the OAuth application ID for this provider from '. 141 + '"%s" to "%s".', 142 + $xaction->renderHandleLink($author_phid), 143 + $old, 144 + $new); 145 + } else { 146 + return pht( 147 + '%s set the OAuth application ID for this provider to '. 148 + '"%s".', 149 + $xaction->renderHandleLink($author_phid), 150 + $new); 151 + } 152 + case self::PROPERTY_APP_SECRET: 153 + if (strlen($old)) { 154 + return pht( 155 + '%s updated the OAuth application secret for this provider.', 156 + $xaction->renderHandleLink($author_phid)); 157 + } else { 158 + return pht( 159 + '%s set the OAuth application secret for this provider.', 160 + $xaction->renderHandleLink($author_phid)); 161 + } 162 + case self::PROPERTY_APP_NOTE: 163 + if (strlen($old)) { 164 + return pht( 165 + '%s updated the OAuth application notes for this provider.', 166 + $xaction->renderHandleLink($author_phid)); 167 + } else { 168 + return pht( 169 + '%s set the OAuth application notes for this provider.', 170 + $xaction->renderHandleLink($author_phid)); 171 + } 172 + 173 + } 174 + 175 + return parent::renderConfigPropertyTransactionTitle($xaction); 176 + } 177 + 178 + protected function synchronizeOAuthAccount( 179 + PhabricatorExternalAccount $account) { 180 + $adapter = $this->getAdapter(); 181 + 182 + $oauth_token = $adapter->getAccessToken(); 183 + $account->setProperty('oauth.token.access', $oauth_token); 184 + 185 + if ($adapter->supportsTokenRefresh()) { 186 + $refresh_token = $adapter->getRefreshToken(); 187 + $account->setProperty('oauth.token.refresh', $refresh_token); 188 + } else { 189 + $account->setProperty('oauth.token.refresh', null); 190 + } 191 + 192 + $expires = $adapter->getAccessTokenExpires(); 193 + $account->setProperty('oauth.token.access.expires', $expires); 194 + } 195 + 196 + public function getOAuthAccessToken( 197 + PhabricatorExternalAccount $account, 198 + $force_refresh = false) { 199 + 200 + if ($account->getProviderKey() !== $this->getProviderKey()) { 201 + throw new Exception("Account does not match provider!"); 202 + } 203 + 204 + if (!$force_refresh) { 205 + $access_expires = $account->getProperty('oauth.token.access.expires'); 206 + $access_token = $account->getProperty('oauth.token.access'); 207 + 208 + // Don't return a token with fewer than this many seconds remaining until 209 + // it expires. 210 + $shortest_token = 60; 211 + if ($access_token) { 212 + if ($access_expires === null || 213 + $access_expires > (time() + $shortest_token)) { 214 + return $access_token; 215 + } 216 + } 217 + } 218 + 219 + $refresh_token = $account->getProperty('oauth.token.refresh'); 220 + if ($refresh_token) { 221 + $adapter = $this->getAdapter(); 222 + if ($adapter->supportsTokenRefresh()) { 223 + $adapter->refreshAccessToken($refresh_token); 224 + 225 + $this->synchronizeOAuthAccount($account); 226 + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); 227 + $account->save(); 228 + unset($unguarded); 229 + 230 + return $account->getProperty('oauth.token.access'); 231 + } 232 + } 233 + 234 + return null; 235 + } 236 + 237 + public function willRenderLinkedAccount( 238 + PhabricatorUser $viewer, 239 + PHUIObjectItemView $item, 240 + PhabricatorExternalAccount $account) { 241 + 242 + // Get a valid token, possibly refreshing it. 243 + $oauth_token = $this->getOAuthAccessToken($account); 244 + 245 + $item->addAttribute(pht('OAuth2 Account')); 246 + 247 + if ($oauth_token) { 248 + $oauth_expires = $account->getProperty('oauth.token.access.expires'); 249 + if ($oauth_expires) { 250 + $item->addAttribute( 251 + pht( 252 + 'Active OAuth Token (Expires: %s)', 253 + phabricator_datetime($oauth_expires, $viewer))); 254 + } else { 255 + $item->addAttribute( 256 + pht( 257 + 'Active OAuth Token')); 258 + } 259 + } else { 260 + $item->addAttribute(pht('No OAuth Access Token')); 261 + } 262 + 263 + parent::willRenderLinkedAccount($viewer, $item, $account); 264 + } 265 + 266 + }
+2 -2
src/applications/auth/provider/PhabricatorAuthProviderOAuthAmazon.php
··· 1 1 <?php 2 2 3 3 final class PhabricatorAuthProviderOAuthAmazon 4 - extends PhabricatorAuthProviderOAuth { 4 + extends PhabricatorAuthProviderOAuth2 { 5 5 6 6 public function getProviderName() { 7 7 return pht('Amazon'); 8 8 } 9 9 10 - public function getConfigurationHelp() { 10 + protected function getProviderConfigurationHelp() { 11 11 $login_uri = PhabricatorEnv::getURI($this->getLoginURI()); 12 12 13 13 $uri = new PhutilURI(PhabricatorEnv::getProductionURI('/'));
+2 -2
src/applications/auth/provider/PhabricatorAuthProviderOAuthAsana.php
··· 1 1 <?php 2 2 3 3 final class PhabricatorAuthProviderOAuthAsana 4 - extends PhabricatorAuthProviderOAuth { 4 + extends PhabricatorAuthProviderOAuth2 { 5 5 6 6 public function getProviderName() { 7 7 return pht('Asana'); 8 8 } 9 9 10 - public function getConfigurationHelp() { 10 + protected function getProviderConfigurationHelp() { 11 11 $app_uri = PhabricatorEnv::getProductionURI('/'); 12 12 $login_uri = PhabricatorEnv::getURI($this->getLoginURI()); 13 13
+2 -2
src/applications/auth/provider/PhabricatorAuthProviderOAuthDisqus.php
··· 1 1 <?php 2 2 3 3 final class PhabricatorAuthProviderOAuthDisqus 4 - extends PhabricatorAuthProviderOAuth { 4 + extends PhabricatorAuthProviderOAuth2 { 5 5 6 6 public function getProviderName() { 7 7 return pht('Disqus'); 8 8 } 9 9 10 - public function getConfigurationHelp() { 10 + protected function getProviderConfigurationHelp() { 11 11 $login_uri = PhabricatorEnv::getURI($this->getLoginURI()); 12 12 13 13 return pht(
+2 -2
src/applications/auth/provider/PhabricatorAuthProviderOAuthFacebook.php
··· 1 1 <?php 2 2 3 3 final class PhabricatorAuthProviderOAuthFacebook 4 - extends PhabricatorAuthProviderOAuth { 4 + extends PhabricatorAuthProviderOAuth2 { 5 5 6 6 const KEY_REQUIRE_SECURE = 'oauth:facebook:require-secure'; 7 7 ··· 9 9 return pht('Facebook'); 10 10 } 11 11 12 - public function getConfigurationHelp() { 12 + protected function getProviderConfigurationHelp() { 13 13 $uri = PhabricatorEnv::getProductionURI($this->getLoginURI()); 14 14 return pht( 15 15 'To configure Facebook OAuth, create a new Facebook Application here:'.
+2 -2
src/applications/auth/provider/PhabricatorAuthProviderOAuthGitHub.php
··· 1 1 <?php 2 2 3 3 final class PhabricatorAuthProviderOAuthGitHub 4 - extends PhabricatorAuthProviderOAuth { 4 + extends PhabricatorAuthProviderOAuth2 { 5 5 6 6 public function getProviderName() { 7 7 return pht('GitHub'); 8 8 } 9 9 10 - public function getConfigurationHelp() { 10 + protected function getProviderConfigurationHelp() { 11 11 $uri = PhabricatorEnv::getProductionURI('/'); 12 12 $callback_uri = PhabricatorEnv::getURI($this->getLoginURI()); 13 13
+4 -2
src/applications/auth/provider/PhabricatorAuthProviderOAuthGoogle.php
··· 1 1 <?php 2 2 3 3 final class PhabricatorAuthProviderOAuthGoogle 4 - extends PhabricatorAuthProviderOAuth { 4 + extends PhabricatorAuthProviderOAuth2 { 5 5 6 6 public function getProviderName() { 7 7 return pht('Google'); 8 8 } 9 9 10 - public function getConfigurationHelp() { 10 + protected function getProviderConfigurationHelp() { 11 11 $login_uri = PhabricatorEnv::getURI($this->getLoginURI()); 12 12 13 13 return pht( ··· 19 19 "\n\n". 20 20 " - Under **APIs & auth > APIs**, scroll down the list and enable ". 21 21 " the **Google+ API**.\n". 22 + " - You will need to consent to the **Google+ API** terms if you ". 23 + " have not before.\n". 22 24 " - Under **APIs & auth > Credentials**, click **Create New Client". 23 25 " ID** in the **OAuth** section. Then use these settings:\n". 24 26 " - **Application Type**: Web Application\n".
+2 -2
src/applications/auth/provider/PhabricatorAuthProviderOAuthTwitch.php
··· 1 1 <?php 2 2 3 3 final class PhabricatorAuthProviderOAuthTwitch 4 - extends PhabricatorAuthProviderOAuth { 4 + extends PhabricatorAuthProviderOAuth2 { 5 5 6 6 public function getProviderName() { 7 7 return pht('Twitch.tv'); 8 8 } 9 9 10 - public function getConfigurationHelp() { 10 + protected function getProviderConfigurationHelp() { 11 11 $login_uri = PhabricatorEnv::getURI($this->getLoginURI()); 12 12 13 13 return pht(