mirror of
https://we.phorge.it/source/phorge.git
synced 2025-01-10 23:01:04 +01:00
Begin cleaning up OAuth scope handling
Summary: Ref T7303. OAuth scope handling never got fully modernized and is a bit of a mess. Also introduce implicit "ALWAYS" and "NEVER" scopes. Always give tokens access to meta-methods like `conduit.getcapabilities` and `conduit.query`. These do not expose user information. Test Plan: - Used a token to call `user.whoami`. - Used a token to call `conduit.query`. - Used a token to try to call `user.query`, got rebuffed. Reviewers: chad Reviewed By: chad Maniphest Tasks: T7303 Differential Revision: https://secure.phabricator.com/D15593
This commit is contained in:
parent
694a8543d8
commit
60133b6fa5
11 changed files with 138 additions and 81 deletions
|
@ -65,10 +65,6 @@ final class ConduitCall extends Phobject {
|
||||||
return $this->handler->shouldAllowUnguardedWrites();
|
return $this->handler->shouldAllowUnguardedWrites();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRequiredScope() {
|
|
||||||
return $this->handler->getRequiredScope();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getErrorDescription($code) {
|
public function getErrorDescription($code) {
|
||||||
return $this->handler->getErrorDescription($code);
|
return $this->handler->getErrorDescription($code);
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,6 @@ final class PhabricatorConduitAPIController
|
||||||
$auth_error = null;
|
$auth_error = null;
|
||||||
$conduit_username = '-';
|
$conduit_username = '-';
|
||||||
if ($call->shouldRequireAuthentication()) {
|
if ($call->shouldRequireAuthentication()) {
|
||||||
$metadata['scope'] = $call->getRequiredScope();
|
|
||||||
$auth_error = $this->authenticateUser($api_request, $metadata, $method);
|
$auth_error = $this->authenticateUser($api_request, $metadata, $method);
|
||||||
// If we've explicitly authenticated the user here and either done
|
// If we've explicitly authenticated the user here and either done
|
||||||
// CSRF validation or are using a non-web authentication mechanism.
|
// CSRF validation or are using a non-web authentication mechanism.
|
||||||
|
@ -185,11 +184,6 @@ final class PhabricatorConduitAPIController
|
||||||
// First, verify the signature.
|
// First, verify the signature.
|
||||||
try {
|
try {
|
||||||
$protocol_data = $metadata;
|
$protocol_data = $metadata;
|
||||||
|
|
||||||
// TODO: We should stop writing this into the protocol data when
|
|
||||||
// processing a request.
|
|
||||||
unset($protocol_data['scope']);
|
|
||||||
|
|
||||||
ConduitClient::verifySignature(
|
ConduitClient::verifySignature(
|
||||||
$method,
|
$method,
|
||||||
$api_request->getAllParameters(),
|
$api_request->getAllParameters(),
|
||||||
|
@ -362,11 +356,8 @@ final class PhabricatorConduitAPIController
|
||||||
$user);
|
$user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle oauth
|
|
||||||
$access_token = idx($metadata, 'access_token');
|
$access_token = idx($metadata, 'access_token');
|
||||||
$method_scope = idx($metadata, 'scope');
|
if ($access_token) {
|
||||||
if ($access_token &&
|
|
||||||
$method_scope != PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE) {
|
|
||||||
$token = id(new PhabricatorOAuthServerAccessToken())
|
$token = id(new PhabricatorOAuthServerAccessToken())
|
||||||
->loadOneWhere('token = %s', $access_token);
|
->loadOneWhere('token = %s', $access_token);
|
||||||
if (!$token) {
|
if (!$token) {
|
||||||
|
@ -377,25 +368,33 @@ final class PhabricatorConduitAPIController
|
||||||
}
|
}
|
||||||
|
|
||||||
$oauth_server = new PhabricatorOAuthServer();
|
$oauth_server = new PhabricatorOAuthServer();
|
||||||
$valid = $oauth_server->validateAccessToken($token,
|
$authorization = $oauth_server->authorizeToken($token);
|
||||||
$method_scope);
|
if (!$authorization) {
|
||||||
if (!$valid) {
|
|
||||||
return array(
|
return array(
|
||||||
'ERR-INVALID-AUTH',
|
'ERR-INVALID-AUTH',
|
||||||
pht('Access token is invalid.'),
|
pht('Access token is invalid or expired.'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// valid token, so let's log in the user!
|
$user = id(new PhabricatorPeopleQuery())
|
||||||
$user_phid = $token->getUserPHID();
|
->setViewer(PhabricatorUser::getOmnipotentUser())
|
||||||
$user = id(new PhabricatorUser())
|
->withPHIDs(array($token->getUserPHID()))
|
||||||
->loadOneWhere('phid = %s', $user_phid);
|
->executeOne();
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
return array(
|
return array(
|
||||||
'ERR-INVALID-AUTH',
|
'ERR-INVALID-AUTH',
|
||||||
pht('Access token is for invalid user.'),
|
pht('Access token is for invalid user.'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$ok = $this->authorizeOAuthMethodAccess($authorization, $method);
|
||||||
|
if (!$ok) {
|
||||||
|
return array(
|
||||||
|
'ERR-OAUTH-ACCESS',
|
||||||
|
pht('You do not have authorization to call this method.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->validateAuthenticatedUser(
|
return $this->validateAuthenticatedUser(
|
||||||
$api_request,
|
$api_request,
|
||||||
$user);
|
$user);
|
||||||
|
@ -642,4 +641,30 @@ final class PhabricatorConduitAPIController
|
||||||
return array($metadata, $params);
|
return array($metadata, $params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function authorizeOAuthMethodAccess(
|
||||||
|
PhabricatorOAuthClientAuthorization $authorization,
|
||||||
|
$method_name) {
|
||||||
|
|
||||||
|
$method = ConduitAPIMethod::getConduitMethod($method_name);
|
||||||
|
if (!$method) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$required_scope = $method->getRequiredScope();
|
||||||
|
switch ($required_scope) {
|
||||||
|
case ConduitAPIMethod::SCOPE_ALWAYS:
|
||||||
|
return true;
|
||||||
|
case ConduitAPIMethod::SCOPE_NEVER:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$authorization_scope = $authorization->getScope();
|
||||||
|
if (!empty($authorization_scope[$required_scope])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,6 +142,36 @@ final class PhabricatorConduitConsoleController
|
||||||
pht('Errors'),
|
pht('Errors'),
|
||||||
$error_description);
|
$error_description);
|
||||||
|
|
||||||
|
|
||||||
|
$scope = $method->getRequiredScope();
|
||||||
|
switch ($scope) {
|
||||||
|
case ConduitAPIMethod::SCOPE_ALWAYS:
|
||||||
|
$oauth_icon = 'fa-globe green';
|
||||||
|
$oauth_description = pht(
|
||||||
|
'OAuth clients may always call this method.');
|
||||||
|
break;
|
||||||
|
case ConduitAPIMethod::SCOPE_NEVER:
|
||||||
|
$oauth_icon = 'fa-ban red';
|
||||||
|
$oauth_description = pht(
|
||||||
|
'OAuth clients may never call this method.');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$oauth_icon = 'fa-unlock-alt blue';
|
||||||
|
$oauth_description = pht(
|
||||||
|
'OAuth clients may call this method after requesting access to '.
|
||||||
|
'the "%s" scope.',
|
||||||
|
$scope);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$view->addProperty(
|
||||||
|
pht('OAuth Scope'),
|
||||||
|
array(
|
||||||
|
id(new PHUIIconView())->setIcon($oauth_icon),
|
||||||
|
' ',
|
||||||
|
$oauth_description,
|
||||||
|
));
|
||||||
|
|
||||||
$view->addSectionHeader(
|
$view->addSectionHeader(
|
||||||
pht('Description'), PHUIPropertyListView::ICON_SUMMARY);
|
pht('Description'), PHUIPropertyListView::ICON_SUMMARY);
|
||||||
$view->addTextContent(
|
$view->addTextContent(
|
||||||
|
|
|
@ -15,6 +15,8 @@ abstract class ConduitAPIMethod
|
||||||
const METHOD_STATUS_UNSTABLE = 'unstable';
|
const METHOD_STATUS_UNSTABLE = 'unstable';
|
||||||
const METHOD_STATUS_DEPRECATED = 'deprecated';
|
const METHOD_STATUS_DEPRECATED = 'deprecated';
|
||||||
|
|
||||||
|
const SCOPE_NEVER = 'scope.never';
|
||||||
|
const SCOPE_ALWAYS = 'scope.always';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a short, human-readable text summary of the method.
|
* Get a short, human-readable text summary of the method.
|
||||||
|
@ -108,8 +110,7 @@ abstract class ConduitAPIMethod
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRequiredScope() {
|
public function getRequiredScope() {
|
||||||
// by default, conduit methods are not accessible via OAuth
|
return self::SCOPE_NEVER;
|
||||||
return PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function executeMethod(ConduitAPIRequest $request) {
|
public function executeMethod(ConduitAPIRequest $request) {
|
||||||
|
|
|
@ -24,6 +24,10 @@ final class ConduitGetCapabilitiesConduitAPIMethod extends ConduitAPIMethod {
|
||||||
return 'dict<string, any>';
|
return 'dict<string, any>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRequiredScope() {
|
||||||
|
return self::SCOPE_ALWAYS;
|
||||||
|
}
|
||||||
|
|
||||||
protected function execute(ConduitAPIRequest $request) {
|
protected function execute(ConduitAPIRequest $request) {
|
||||||
$authentication = array(
|
$authentication = array(
|
||||||
'token',
|
'token',
|
||||||
|
|
|
@ -18,6 +18,10 @@ final class ConduitQueryConduitAPIMethod extends ConduitAPIMethod {
|
||||||
return 'dict<dict>';
|
return 'dict<dict>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getRequiredScope() {
|
||||||
|
return self::SCOPE_ALWAYS;
|
||||||
|
}
|
||||||
|
|
||||||
protected function execute(ConduitAPIRequest $request) {
|
protected function execute(ConduitAPIRequest $request) {
|
||||||
$methods = id(new PhabricatorConduitMethodQuery())
|
$methods = id(new PhabricatorConduitMethodQuery())
|
||||||
->setViewer($request->getUser())
|
->setViewer($request->getUser())
|
||||||
|
|
|
@ -29,7 +29,6 @@
|
||||||
final class PhabricatorOAuthServer extends Phobject {
|
final class PhabricatorOAuthServer extends Phobject {
|
||||||
|
|
||||||
const AUTHORIZATION_CODE_TIMEOUT = 300;
|
const AUTHORIZATION_CODE_TIMEOUT = 300;
|
||||||
const ACCESS_TOKEN_TIMEOUT = 3600;
|
|
||||||
|
|
||||||
private $user;
|
private $user;
|
||||||
private $client;
|
private $client;
|
||||||
|
@ -158,39 +157,35 @@ final class PhabricatorOAuthServer extends Phobject {
|
||||||
/**
|
/**
|
||||||
* @task token
|
* @task token
|
||||||
*/
|
*/
|
||||||
public function validateAccessToken(
|
public function authorizeToken(
|
||||||
PhabricatorOAuthServerAccessToken $token,
|
PhabricatorOAuthServerAccessToken $token) {
|
||||||
$required_scope) {
|
|
||||||
|
|
||||||
$created_time = $token->getDateCreated();
|
$user_phid = $token->getUserPHID();
|
||||||
$must_be_used_by = $created_time + self::ACCESS_TOKEN_TIMEOUT;
|
$client_phid = $token->getClientPHID();
|
||||||
$expired = time() > $must_be_used_by;
|
|
||||||
$authorization = id(new PhabricatorOAuthClientAuthorization())
|
|
||||||
->loadOneWhere(
|
|
||||||
'userPHID = %s AND clientPHID = %s',
|
|
||||||
$token->getUserPHID(),
|
|
||||||
$token->getClientPHID());
|
|
||||||
|
|
||||||
|
$authorization = id(new PhabricatorOAuthClientAuthorizationQuery())
|
||||||
|
->setViewer(PhabricatorUser::getOmnipotentUser())
|
||||||
|
->withUserPHIDs(array($user_phid))
|
||||||
|
->withClientPHIDs(array($client_phid))
|
||||||
|
->executeOne();
|
||||||
if (!$authorization) {
|
if (!$authorization) {
|
||||||
return false;
|
return null;
|
||||||
}
|
|
||||||
$token_scope = $authorization->getScope();
|
|
||||||
if (!isset($token_scope[$required_scope])) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$valid = true;
|
// TODO: This should probably be reworked; expiration should be an
|
||||||
if ($expired) {
|
// exclusive property of the token. For now, this logic reads: tokens for
|
||||||
$valid = false;
|
// authorizations with "offline_access" never expire.
|
||||||
// check if the scope includes "offline_access", which makes the
|
|
||||||
// token valid despite being expired
|
$is_expired = $token->isExpired();
|
||||||
if (isset(
|
if ($is_expired) {
|
||||||
$token_scope[PhabricatorOAuthServerScope::SCOPE_OFFLINE_ACCESS])) {
|
$offline_access = PhabricatorOAuthServerScope::SCOPE_OFFLINE_ACCESS;
|
||||||
$valid = true;
|
$authorization_scope = $authorization->getScope();
|
||||||
|
if (empty($authorization_scope[$offline_access])) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $valid;
|
return $authorization;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -4,13 +4,7 @@ final class PhabricatorOAuthServerScope extends Phobject {
|
||||||
|
|
||||||
const SCOPE_OFFLINE_ACCESS = 'offline_access';
|
const SCOPE_OFFLINE_ACCESS = 'offline_access';
|
||||||
const SCOPE_WHOAMI = 'whoami';
|
const SCOPE_WHOAMI = 'whoami';
|
||||||
const SCOPE_NOT_ACCESSIBLE = 'not_accessible';
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Note this does not contain SCOPE_NOT_ACCESSIBLE which is magic
|
|
||||||
* used to simplify code for data that is not currently accessible
|
|
||||||
* via OAuth.
|
|
||||||
*/
|
|
||||||
public static function getScopesDict() {
|
public static function getScopesDict() {
|
||||||
return array(
|
return array(
|
||||||
self::SCOPE_OFFLINE_ACCESS => 1,
|
self::SCOPE_OFFLINE_ACCESS => 1,
|
||||||
|
|
|
@ -144,7 +144,7 @@ final class PhabricatorOAuthServerTokenController
|
||||||
$result = array(
|
$result = array(
|
||||||
'access_token' => $access_token->getToken(),
|
'access_token' => $access_token->getToken(),
|
||||||
'token_type' => 'Bearer',
|
'token_type' => 'Bearer',
|
||||||
'expires_in' => PhabricatorOAuthServer::ACCESS_TOKEN_TIMEOUT,
|
'expires_in' => $access_token->getExpiresDuration(),
|
||||||
);
|
);
|
||||||
return $response->setContent($result);
|
return $response->setContent($result);
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
|
|
|
@ -7,7 +7,7 @@ final class PhabricatorOAuthClientAuthorizationQuery
|
||||||
private $userPHIDs;
|
private $userPHIDs;
|
||||||
private $clientPHIDs;
|
private $clientPHIDs;
|
||||||
|
|
||||||
public function witHPHIDs(array $phids) {
|
public function withPHIDs(array $phids) {
|
||||||
$this->phids = $phids;
|
$this->phids = $phids;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
@ -22,19 +22,12 @@ final class PhabricatorOAuthClientAuthorizationQuery
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function newResultObject() {
|
||||||
|
return new PhabricatorOAuthClientAuthorization();
|
||||||
|
}
|
||||||
|
|
||||||
protected function loadPage() {
|
protected function loadPage() {
|
||||||
$table = new PhabricatorOAuthClientAuthorization();
|
return $this->loadStandardPage($this->newResultObject());
|
||||||
$conn_r = $table->establishConnection('r');
|
|
||||||
|
|
||||||
$data = queryfx_all(
|
|
||||||
$conn_r,
|
|
||||||
'SELECT * FROM %T auth %Q %Q %Q',
|
|
||||||
$table->getTableName(),
|
|
||||||
$this->buildWhereClause($conn_r),
|
|
||||||
$this->buildOrderClause($conn_r),
|
|
||||||
$this->buildLimitClause($conn_r));
|
|
||||||
|
|
||||||
return $table->loadAllFromArray($data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function willFilterPage(array $authorizations) {
|
protected function willFilterPage(array $authorizations) {
|
||||||
|
@ -49,43 +42,44 @@ final class PhabricatorOAuthClientAuthorizationQuery
|
||||||
|
|
||||||
foreach ($authorizations as $key => $authorization) {
|
foreach ($authorizations as $key => $authorization) {
|
||||||
$client = idx($clients, $authorization->getClientPHID());
|
$client = idx($clients, $authorization->getClientPHID());
|
||||||
|
|
||||||
if (!$client) {
|
if (!$client) {
|
||||||
|
$this->didRejectResult($authorization);
|
||||||
unset($authorizations[$key]);
|
unset($authorizations[$key]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$authorization->attachClient($client);
|
$authorization->attachClient($client);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $authorizations;
|
return $authorizations;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
|
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
|
||||||
$where = array();
|
$where = parent::buildWhereClauseParts($conn);
|
||||||
|
|
||||||
if ($this->phids) {
|
if ($this->phids !== null) {
|
||||||
$where[] = qsprintf(
|
$where[] = qsprintf(
|
||||||
$conn_r,
|
$conn,
|
||||||
'phid IN (%Ls)',
|
'phid IN (%Ls)',
|
||||||
$this->phids);
|
$this->phids);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->userPHIDs) {
|
if ($this->userPHIDs !== null) {
|
||||||
$where[] = qsprintf(
|
$where[] = qsprintf(
|
||||||
$conn_r,
|
$conn,
|
||||||
'userPHID IN (%Ls)',
|
'userPHID IN (%Ls)',
|
||||||
$this->userPHIDs);
|
$this->userPHIDs);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->clientPHIDs) {
|
if ($this->clientPHIDs !== null) {
|
||||||
$where[] = qsprintf(
|
$where[] = qsprintf(
|
||||||
$conn_r,
|
$conn,
|
||||||
'clientPHID IN (%Ls)',
|
'clientPHID IN (%Ls)',
|
||||||
$this->clientPHIDs);
|
$this->clientPHIDs);
|
||||||
}
|
}
|
||||||
|
|
||||||
$where[] = $this->buildPagingClause($conn_r);
|
return $where;
|
||||||
|
|
||||||
return $this->formatWhereClause($where);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getQueryApplicationClass() {
|
public function getQueryApplicationClass() {
|
||||||
|
|
|
@ -22,4 +22,18 @@ final class PhabricatorOAuthServerAccessToken
|
||||||
) + parent::getConfiguration();
|
) + parent::getConfiguration();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isExpired() {
|
||||||
|
$now = PhabricatorTime::getNow();
|
||||||
|
$expires_epoch = $this->getExpiresEpoch();
|
||||||
|
return ($now > $expires_epoch);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExpiresEpoch() {
|
||||||
|
return $this->getDateCreated() + 3600;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExpiresDuration() {
|
||||||
|
return PhabricatorTime::getNow() - $this->getExpiresEpoch();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue