Path: blob/master/src/applications/oauthserver/PhabricatorOAuthServer.php
12241 views
<?php12/**3* Implements core OAuth 2.0 Server logic.4*5* This class should be used behind business logic that parses input to6* determine pertinent @{class:PhabricatorUser} $user,7* @{class:PhabricatorOAuthServerClient} $client(s),8* @{class:PhabricatorOAuthServerAuthorizationCode} $code(s), and.9* @{class:PhabricatorOAuthServerAccessToken} $token(s).10*11* For an OAuth 2.0 server, there are two main steps:12*13* 1) Authorization - the user authorizes a given client to access the data14* the OAuth 2.0 server protects. Once this is achieved / if it has15* been achived already, the OAuth server sends the client an authorization16* code.17* 2) Access Token - the client should send the authorization code received in18* step 1 along with its id and secret to the OAuth server to receive an19* access token. This access token can later be used to access Phabricator20* data on behalf of the user.21*22* @task auth Authorizing @{class:PhabricatorOAuthServerClient}s and23* generating @{class:PhabricatorOAuthServerAuthorizationCode}s24* @task token Validating @{class:PhabricatorOAuthServerAuthorizationCode}s25* and generating @{class:PhabricatorOAuthServerAccessToken}s26* @task internal Internals27*/28final class PhabricatorOAuthServer extends Phobject {2930const AUTHORIZATION_CODE_TIMEOUT = 300;3132private $user;33private $client;3435private function getUser() {36if (!$this->user) {37throw new PhutilInvalidStateException('setUser');38}39return $this->user;40}4142public function setUser(PhabricatorUser $user) {43$this->user = $user;44return $this;45}4647private function getClient() {48if (!$this->client) {49throw new PhutilInvalidStateException('setClient');50}51return $this->client;52}5354public function setClient(PhabricatorOAuthServerClient $client) {55$this->client = $client;56return $this;57}5859/**60* @task auth61* @return tuple <bool hasAuthorized, ClientAuthorization or null>62*/63public function userHasAuthorizedClient(array $scope) {6465$authorization = id(new PhabricatorOAuthClientAuthorization())66->loadOneWhere(67'userPHID = %s AND clientPHID = %s',68$this->getUser()->getPHID(),69$this->getClient()->getPHID());70if (empty($authorization)) {71return array(false, null);72}7374if ($scope) {75$missing_scope = array_diff_key($scope, $authorization->getScope());76} else {77$missing_scope = false;78}7980if ($missing_scope) {81return array(false, $authorization);82}8384return array(true, $authorization);85}8687/**88* @task auth89*/90public function authorizeClient(array $scope) {91$authorization = new PhabricatorOAuthClientAuthorization();92$authorization->setUserPHID($this->getUser()->getPHID());93$authorization->setClientPHID($this->getClient()->getPHID());94$authorization->setScope($scope);95$authorization->save();9697return $authorization;98}99100/**101* @task auth102*/103public function generateAuthorizationCode(PhutilURI $redirect_uri) {104105$code = Filesystem::readRandomCharacters(32);106$client = $this->getClient();107108$authorization_code = new PhabricatorOAuthServerAuthorizationCode();109$authorization_code->setCode($code);110$authorization_code->setClientPHID($client->getPHID());111$authorization_code->setClientSecret($client->getSecret());112$authorization_code->setUserPHID($this->getUser()->getPHID());113$authorization_code->setRedirectURI((string)$redirect_uri);114$authorization_code->save();115116return $authorization_code;117}118119/**120* @task token121*/122public function generateAccessToken() {123124$token = Filesystem::readRandomCharacters(32);125126$access_token = new PhabricatorOAuthServerAccessToken();127$access_token->setToken($token);128$access_token->setUserPHID($this->getUser()->getPHID());129$access_token->setClientPHID($this->getClient()->getPHID());130$access_token->save();131132return $access_token;133}134135/**136* @task token137*/138public function validateAuthorizationCode(139PhabricatorOAuthServerAuthorizationCode $test_code,140PhabricatorOAuthServerAuthorizationCode $valid_code) {141142// check that all the meta data matches143if ($test_code->getClientPHID() != $valid_code->getClientPHID()) {144return false;145}146if ($test_code->getClientSecret() != $valid_code->getClientSecret()) {147return false;148}149150// check that the authorization code hasn't timed out151$created_time = $test_code->getDateCreated();152$must_be_used_by = $created_time + self::AUTHORIZATION_CODE_TIMEOUT;153return (time() < $must_be_used_by);154}155156/**157* @task token158*/159public function authorizeToken(160PhabricatorOAuthServerAccessToken $token) {161162$user_phid = $token->getUserPHID();163$client_phid = $token->getClientPHID();164165$authorization = id(new PhabricatorOAuthClientAuthorizationQuery())166->setViewer(PhabricatorUser::getOmnipotentUser())167->withUserPHIDs(array($user_phid))168->withClientPHIDs(array($client_phid))169->executeOne();170if (!$authorization) {171return null;172}173174$application = $authorization->getClient();175if ($application->getIsDisabled()) {176return null;177}178179return $authorization;180}181182public function validateRedirectURI($uri) {183try {184$this->assertValidRedirectURI($uri);185return true;186} catch (Exception $ex) {187return false;188}189}190191/**192* See http://tools.ietf.org/html/draft-ietf-oauth-v2-23#section-3.1.2193* for details on what makes a given redirect URI "valid".194*/195public function assertValidRedirectURI($raw_uri) {196// This covers basics like reasonable formatting and the existence of a197// protocol.198PhabricatorEnv::requireValidRemoteURIForLink($raw_uri);199200$uri = new PhutilURI($raw_uri);201202$fragment = $uri->getFragment();203if (strlen($fragment)) {204throw new Exception(205pht(206'OAuth application redirect URIs must not contain URI '.207'fragments, but the URI "%s" has a fragment ("%s").',208$raw_uri,209$fragment));210}211212$protocol = $uri->getProtocol();213switch ($protocol) {214case 'http':215case 'https':216break;217default:218throw new Exception(219pht(220'OAuth application redirect URIs must only use the "http" or '.221'"https" protocols, but the URI "%s" uses the "%s" protocol.',222$raw_uri,223$protocol));224}225}226227/**228* If there's a URI specified in an OAuth request, it must be validated in229* its own right. Further, it must have the same domain, the same path, the230* same port, and (at least) the same query parameters as the primary URI.231*/232public function validateSecondaryRedirectURI(233PhutilURI $secondary_uri,234PhutilURI $primary_uri) {235236// The secondary URI must be valid.237if (!$this->validateRedirectURI($secondary_uri)) {238return false;239}240241// Both URIs must point at the same domain.242if ($secondary_uri->getDomain() != $primary_uri->getDomain()) {243return false;244}245246// Both URIs must have the same path247if ($secondary_uri->getPath() != $primary_uri->getPath()) {248return false;249}250251// Both URIs must have the same port252if ($secondary_uri->getPort() != $primary_uri->getPort()) {253return false;254}255256// Any query parameters present in the first URI must be exactly present257// in the second URI.258$need_params = $primary_uri->getQueryParamsAsMap();259$have_params = $secondary_uri->getQueryParamsAsMap();260261foreach ($need_params as $key => $value) {262if (!array_key_exists($key, $have_params)) {263return false;264}265if ((string)$have_params[$key] != (string)$value) {266return false;267}268}269270// If the first URI is HTTPS, the second URI must also be HTTPS. This271// defuses an attack where a third party with control over the network272// tricks you into using HTTP to authenticate over a link which is supposed273// to be HTTPS only and sniffs all your token cookies.274if (strtolower($primary_uri->getProtocol()) == 'https') {275if (strtolower($secondary_uri->getProtocol()) != 'https') {276return false;277}278}279280return true;281}282283}284285286