1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-10 00:42:41 +01:00

Add Twitter as an authentication provider

Summary: Ref T3687. Depends on D6864. Implements the `OAuth1` provider in Phabricator (which is mostly similar to the OAuth2 provider, but doesn't share quite enough code to actually extend a common base class, I think) and Twitter as a concrete subclass.

Test Plan:
Created a Twitter provider. Registered, logged in, linked, refreshed account link.

{F57054}

{F57056}

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T3687

Differential Revision: https://secure.phabricator.com/D6865
This commit is contained in:
epriestley 2013-09-03 05:53:08 -07:00
parent 63d2b11cab
commit 25e43e872b
3 changed files with 331 additions and 0 deletions

View file

@ -902,6 +902,8 @@ phutil_register_library_map(array(
'PhabricatorAuthProviderConfigTransactionQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigTransactionQuery.php',
'PhabricatorAuthProviderLDAP' => 'applications/auth/provider/PhabricatorAuthProviderLDAP.php',
'PhabricatorAuthProviderOAuth' => 'applications/auth/provider/PhabricatorAuthProviderOAuth.php',
'PhabricatorAuthProviderOAuth1' => 'applications/auth/provider/PhabricatorAuthProviderOAuth1.php',
'PhabricatorAuthProviderOAuth1Twitter' => 'applications/auth/provider/PhabricatorAuthProviderOAuth1Twitter.php',
'PhabricatorAuthProviderOAuthAmazon' => 'applications/auth/provider/PhabricatorAuthProviderOAuthAmazon.php',
'PhabricatorAuthProviderOAuthAsana' => 'applications/auth/provider/PhabricatorAuthProviderOAuthAsana.php',
'PhabricatorAuthProviderOAuthDisqus' => 'applications/auth/provider/PhabricatorAuthProviderOAuthDisqus.php',
@ -2954,6 +2956,8 @@ phutil_register_library_map(array(
'PhabricatorAuthProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
'PhabricatorAuthProviderLDAP' => 'PhabricatorAuthProvider',
'PhabricatorAuthProviderOAuth' => 'PhabricatorAuthProvider',
'PhabricatorAuthProviderOAuth1' => 'PhabricatorAuthProvider',
'PhabricatorAuthProviderOAuth1Twitter' => 'PhabricatorAuthProviderOAuth1',
'PhabricatorAuthProviderOAuthAmazon' => 'PhabricatorAuthProviderOAuth',
'PhabricatorAuthProviderOAuthAsana' => 'PhabricatorAuthProviderOAuth',
'PhabricatorAuthProviderOAuthDisqus' => 'PhabricatorAuthProviderOAuth',

View file

@ -0,0 +1,292 @@
<?php
abstract class PhabricatorAuthProviderOAuth1 extends PhabricatorAuthProvider {
protected $adapter;
const PROPERTY_CONSUMER_KEY = 'oauth1:consumer:key';
const PROPERTY_CONSUMER_SECRET = 'oauth1:consumer:secret';
const PROPERTY_PRIVATE_KEY = 'oauth1:private:key';
abstract protected function newOAuthAdapter();
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;
}
protected function configureAdapter(PhutilAuthAdapterOAuth1 $adapter) {
$config = $this->getProviderConfig();
$adapter->setConsumerKey($config->getProperty(self::PROPERTY_CONSUMER_KEY));
$adapter->setConsumerSecret(
new PhutilOpaqueEnvelope(
$config->getProperty(self::PROPERTY_CONSUMER_SECRET)));
$adapter->setCallbackURI($this->getLoginURI());
return $adapter;
}
public function isLoginFormAButton() {
return true;
}
protected function renderLoginForm(AphrontRequest $request, $mode) {
$viewer = $request->getUser();
if ($mode == 'link') {
$button_text = pht('Link External Account');
} else if ($mode == 'refresh') {
$button_text = pht('Refresh Account Link');
} else if ($this->shouldAllowRegistration()) {
$button_text = pht('Login or Register');
} else {
$button_text = pht('Login');
}
$icon = id(new PHUIIconView())
->setSpriteSheet(PHUIIconView::SPRITE_LOGIN)
->setSpriteIcon($this->getLoginIcon());
$button = id(new PHUIButtonView())
->setSize(PHUIButtonView::BIG)
->setColor(PHUIButtonView::GREY)
->setIcon($icon)
->setText($button_text)
->setSubtext($this->getProviderName());
$adapter = $this->getAdapter();
$uri = new PhutilURI($this->getLoginURI());
$params = $uri->getQueryParams();
$uri->setQueryParams(array());
$content = array($button);
foreach ($params as $key => $value) {
$content[] = phutil_tag(
'input',
array(
'type' => 'hidden',
'name' => $key,
'value' => $value,
));
}
return phabricator_form(
$viewer,
array(
'method' => 'POST',
'action' => (string)$uri,
),
$content);
}
public function processLoginRequest(
PhabricatorAuthLoginController $controller) {
$request = $controller->getRequest();
$adapter = $this->getAdapter();
$account = null;
$response = null;
if ($request->isHTTPPost()) {
$uri = $adapter->getClientRedirectURI();
$response = id(new AphrontRedirectResponse())->setURI($uri);
return array($account, $response);
}
// NOTE: You can get here via GET, this should probably be a bit more
// user friendly.
$token = $request->getStr('oauth_token');
$verifier = $request->getStr('oauth_verifier');
if (!$token) {
throw new Exception("Expected 'oauth_token' in request!");
}
if (!$verifier) {
throw new Exception("Expected 'oauth_verifier' in request!");
}
$adapter->setToken($token);
$adapter->setVerifier($verifier);
// 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);
}
public function readFormValuesFromProvider() {
$config = $this->getProviderConfig();
$id = $config->getProperty(self::PROPERTY_CONSUMER_KEY);
$secret = $config->getProperty(self::PROPERTY_CONSUMER_SECRET);
return array(
self::PROPERTY_CONSUMER_KEY => $id,
self::PROPERTY_CONSUMER_SECRET => $secret,
);
}
public function readFormValuesFromRequest(AphrontRequest $request) {
return array(
self::PROPERTY_CONSUMER_KEY
=> $request->getStr(self::PROPERTY_CONSUMER_KEY),
self::PROPERTY_CONSUMER_SECRET
=> $request->getStr(self::PROPERTY_CONSUMER_SECRET),
);
}
public function processEditForm(
AphrontRequest $request,
array $values) {
$errors = array();
$issues = array();
$key_ckey = self::PROPERTY_CONSUMER_KEY;
$key_csecret = self::PROPERTY_CONSUMER_SECRET;
if (!strlen($values[$key_ckey])) {
$errors[] = pht('Consumer key is required.');
$issues[$key_ckey] = pht('Required');
}
if (!strlen($values[$key_csecret])) {
$errors[] = pht('Consumer secret is required.');
$issues[$key_csecret] = 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_csecret])) {
unset($values[$key_csecret]);
}
return array($errors, $issues, $values);
}
public function extendEditForm(
AphrontRequest $request,
AphrontFormView $form,
array $values,
array $issues) {
$key_id = self::PROPERTY_CONSUMER_KEY;
$key_secret = self::PROPERTY_CONSUMER_SECRET;
$v_id = $values[$key_id];
$v_secret = $values[$key_secret];
if ($v_secret) {
$v_secret = str_repeat('*', strlen($v_secret));
}
$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(pht('OAuth Consumer Key'))
->setName($key_id)
->setValue($v_id)
->setError($e_id))
->appendChild(
id(new AphrontFormPasswordControl())
->setLabel(pht('OAuth Consumer Secret'))
->setName($key_secret)
->setValue($v_secret)
->setError($e_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)) {
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)) {
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 willSaveAccount(PhabricatorExternalAccount $account) {
parent::willSaveAccount($account);
$this->synchronizeOAuthAccount($account);
}
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,
PhabricatorObjectItemView $item,
PhabricatorExternalAccount $account) {
$item->addAttribute(pht('OAuth1 Account'));
parent::willRenderLinkedAccount($viewer, $item, $account);
}
}

View file

@ -0,0 +1,35 @@
<?php
final class PhabricatorAuthProviderOAuth1Twitter
extends PhabricatorAuthProviderOAuth1 {
public function getProviderName() {
return pht('Twitter');
}
public function getConfigurationHelp() {
$login_uri = $this->getLoginURI();
return pht(
"To configure Twitter OAuth, create a new application here:".
"\n\n".
"https://dev.twitter.com/apps".
"\n\n".
"When creating your application, use these settings:".
"\n\n".
" - **Callback URL:** Set this to: `%s`".
"\n\n".
"After completing configuration, copy the **Consumer Key** and ".
"**Consumer Secret** to the fields above.",
$login_uri);
}
protected function newOAuthAdapter() {
return new PhutilAuthAdapterOAuthTwitter();
}
protected function getLoginIcon() {
return 'Twitter';
}
}