2013-09-03 14:53:08 +02:00
|
|
|
<?php
|
|
|
|
|
2014-07-22 13:04:13 +02:00
|
|
|
abstract class PhabricatorOAuth1AuthProvider
|
|
|
|
extends PhabricatorOAuthAuthProvider {
|
2013-09-03 14:53:08 +02:00
|
|
|
|
|
|
|
protected $adapter;
|
|
|
|
|
|
|
|
const PROPERTY_CONSUMER_KEY = 'oauth1:consumer:key';
|
|
|
|
const PROPERTY_CONSUMER_SECRET = 'oauth1:consumer:secret';
|
|
|
|
const PROPERTY_PRIVATE_KEY = 'oauth1:private:key';
|
|
|
|
|
2014-04-09 20:09:50 +02:00
|
|
|
protected function getIDKey() {
|
|
|
|
return self::PROPERTY_CONSUMER_KEY;
|
2013-09-03 14:53:08 +02:00
|
|
|
}
|
|
|
|
|
2014-04-09 20:09:50 +02:00
|
|
|
protected function getSecretKey() {
|
|
|
|
return self::PROPERTY_CONSUMER_SECRET;
|
2013-09-03 14:53:08 +02:00
|
|
|
}
|
|
|
|
|
2014-07-22 13:04:13 +02:00
|
|
|
protected function configureAdapter(PhutilOAuth1AuthAdapter $adapter) {
|
2013-09-03 14:53:08 +02:00
|
|
|
$config = $this->getProviderConfig();
|
|
|
|
$adapter->setConsumerKey($config->getProperty(self::PROPERTY_CONSUMER_KEY));
|
2013-09-03 14:53:21 +02:00
|
|
|
$secret = $config->getProperty(self::PROPERTY_CONSUMER_SECRET);
|
|
|
|
if (strlen($secret)) {
|
|
|
|
$adapter->setConsumerSecret(new PhutilOpaqueEnvelope($secret));
|
|
|
|
}
|
2014-01-23 23:03:44 +01:00
|
|
|
$adapter->setCallbackURI(PhabricatorEnv::getURI($this->getLoginURI()));
|
2013-09-03 14:53:08 +02:00
|
|
|
return $adapter;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function renderLoginForm(AphrontRequest $request, $mode) {
|
2013-09-03 19:30:53 +02:00
|
|
|
$attributes = array(
|
|
|
|
'method' => 'POST',
|
|
|
|
'uri' => $this->getLoginURI(),
|
|
|
|
);
|
|
|
|
return $this->renderStandardLoginButton($request, $mode, $attributes);
|
2013-09-03 14:53:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function processLoginRequest(
|
|
|
|
PhabricatorAuthLoginController $controller) {
|
|
|
|
|
|
|
|
$request = $controller->getRequest();
|
|
|
|
$adapter = $this->getAdapter();
|
|
|
|
$account = null;
|
|
|
|
$response = null;
|
|
|
|
|
|
|
|
if ($request->isHTTPPost()) {
|
2014-02-24 01:39:24 +01:00
|
|
|
// 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);
|
|
|
|
|
2013-09-03 14:53:08 +02:00
|
|
|
$uri = $adapter->getClientRedirectURI();
|
2014-06-28 14:00:52 +02:00
|
|
|
|
|
|
|
$this->saveHandshakeTokenSecret(
|
|
|
|
$client_code,
|
|
|
|
$adapter->getTokenSecret());
|
|
|
|
|
2014-08-18 23:11:06 +02:00
|
|
|
$response = id(new AphrontRedirectResponse())
|
|
|
|
->setIsExternal(true)
|
|
|
|
->setURI($uri);
|
2013-09-03 14:53:08 +02:00
|
|
|
return array($account, $response);
|
|
|
|
}
|
|
|
|
|
2013-09-03 19:30:39 +02:00
|
|
|
$denied = $request->getStr('denied');
|
|
|
|
if (strlen($denied)) {
|
|
|
|
// Twitter indicates that the user cancelled the login attempt by
|
|
|
|
// returning "denied" as a parameter.
|
|
|
|
throw new PhutilAuthUserAbortedException();
|
|
|
|
}
|
|
|
|
|
2013-09-03 14:53:08 +02:00
|
|
|
// NOTE: You can get here via GET, this should probably be a bit more
|
|
|
|
// user friendly.
|
|
|
|
|
2014-02-24 01:39:24 +01:00
|
|
|
$this->verifyAuthCSRFCode($request, $controller->getExtraURIData());
|
|
|
|
|
2013-09-03 14:53:08 +02:00
|
|
|
$token = $request->getStr('oauth_token');
|
|
|
|
$verifier = $request->getStr('oauth_verifier');
|
|
|
|
|
|
|
|
if (!$token) {
|
2015-05-22 09:27:56 +02:00
|
|
|
throw new Exception(pht("Expected '%s' in request!", 'oauth_token'));
|
2013-09-03 14:53:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!$verifier) {
|
2015-05-22 09:27:56 +02:00
|
|
|
throw new Exception(pht("Expected '%s' in request!", 'oauth_verifier'));
|
2013-09-03 14:53:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$adapter->setToken($token);
|
|
|
|
$adapter->setVerifier($verifier);
|
|
|
|
|
2014-06-28 14:00:52 +02:00
|
|
|
$client_code = $this->getAuthCSRFCode($request);
|
|
|
|
$token_secret = $this->loadHandshakeTokenSecret($client_code);
|
|
|
|
$adapter->setTokenSecret($token_secret);
|
|
|
|
|
2013-09-03 14:53:08 +02:00
|
|
|
// 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 processEditForm(
|
|
|
|
AphrontRequest $request,
|
|
|
|
array $values) {
|
|
|
|
|
|
|
|
$key_ckey = self::PROPERTY_CONSUMER_KEY;
|
|
|
|
$key_csecret = self::PROPERTY_CONSUMER_SECRET;
|
|
|
|
|
2014-04-09 20:09:50 +02:00
|
|
|
return $this->processOAuthEditForm(
|
|
|
|
$request,
|
|
|
|
$values,
|
|
|
|
pht('Consumer key is required.'),
|
|
|
|
pht('Consumer secret is required.'));
|
2013-09-03 14:53:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public function extendEditForm(
|
|
|
|
AphrontRequest $request,
|
|
|
|
AphrontFormView $form,
|
|
|
|
array $values,
|
|
|
|
array $issues) {
|
|
|
|
|
2014-04-09 20:09:50 +02:00
|
|
|
return $this->extendOAuthEditForm(
|
|
|
|
$request,
|
|
|
|
$form,
|
|
|
|
$values,
|
|
|
|
$issues,
|
|
|
|
pht('OAuth Consumer Key'),
|
|
|
|
pht('OAuth Consumer Secret'));
|
2013-09-03 14:53:08 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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 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,
|
2013-09-09 23:14:34 +02:00
|
|
|
PHUIObjectItemView $item,
|
2013-09-03 14:53:08 +02:00
|
|
|
PhabricatorExternalAccount $account) {
|
|
|
|
|
|
|
|
$item->addAttribute(pht('OAuth1 Account'));
|
|
|
|
|
|
|
|
parent::willRenderLinkedAccount($viewer, $item, $account);
|
|
|
|
}
|
|
|
|
|
2014-06-28 14:00:52 +02:00
|
|
|
|
|
|
|
/* -( Temporary Secrets )-------------------------------------------------- */
|
|
|
|
|
|
|
|
|
|
|
|
private function saveHandshakeTokenSecret($client_code, $secret) {
|
2016-03-16 14:36:04 +01:00
|
|
|
$secret_type = PhabricatorOAuth1SecretTemporaryTokenType::TOKENTYPE;
|
2014-06-28 14:00:52 +02:00
|
|
|
$key = $this->getHandshakeTokenKeyFromClientCode($client_code);
|
2016-03-16 14:36:04 +01:00
|
|
|
$type = $this->getTemporaryTokenType($secret_type);
|
2014-06-28 14:00:52 +02:00
|
|
|
|
|
|
|
// Wipe out an existing token, if one exists.
|
|
|
|
$token = id(new PhabricatorAuthTemporaryTokenQuery())
|
|
|
|
->setViewer(PhabricatorUser::getOmnipotentUser())
|
2016-03-16 13:17:47 +01:00
|
|
|
->withTokenResources(array($key))
|
2014-06-28 14:00:52 +02:00
|
|
|
->withTokenTypes(array($type))
|
|
|
|
->executeOne();
|
|
|
|
if ($token) {
|
|
|
|
$token->delete();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Save the new secret.
|
|
|
|
id(new PhabricatorAuthTemporaryToken())
|
2016-03-16 13:17:47 +01:00
|
|
|
->setTokenResource($key)
|
2014-06-28 14:00:52 +02:00
|
|
|
->setTokenType($type)
|
|
|
|
->setTokenExpires(time() + phutil_units('1 hour in seconds'))
|
|
|
|
->setTokenCode($secret)
|
|
|
|
->save();
|
|
|
|
}
|
|
|
|
|
|
|
|
private function loadHandshakeTokenSecret($client_code) {
|
2016-03-16 14:36:04 +01:00
|
|
|
$secret_type = PhabricatorOAuth1SecretTemporaryTokenType::TOKENTYPE;
|
2014-06-28 14:00:52 +02:00
|
|
|
$key = $this->getHandshakeTokenKeyFromClientCode($client_code);
|
2016-03-16 14:36:04 +01:00
|
|
|
$type = $this->getTemporaryTokenType($secret_type);
|
2014-06-28 14:00:52 +02:00
|
|
|
|
|
|
|
$token = id(new PhabricatorAuthTemporaryTokenQuery())
|
|
|
|
->setViewer(PhabricatorUser::getOmnipotentUser())
|
2016-03-16 13:17:47 +01:00
|
|
|
->withTokenResources(array($key))
|
2014-06-28 14:00:52 +02:00
|
|
|
->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.
|
|
|
|
|
2016-03-16 14:36:04 +01:00
|
|
|
// TODO: This isn't really a proper use of the table and should get
|
|
|
|
// cleaned up some day: the type should be constant.
|
|
|
|
|
2014-06-28 14:00:52 +02:00
|
|
|
return $core_type.':'.$this->getProviderConfig()->getID();
|
|
|
|
}
|
|
|
|
|
|
|
|
private function getHandshakeTokenKeyFromClientCode($client_code) {
|
|
|
|
// NOTE: This is very slightly coersive since the TemporaryToken table
|
|
|
|
// expects an "objectPHID" as an identifier, but nothing about the storage
|
|
|
|
// is bound to PHIDs.
|
|
|
|
|
|
|
|
return 'oauth1:secret/'.$client_code;
|
|
|
|
}
|
|
|
|
|
2013-09-03 14:53:08 +02:00
|
|
|
}
|