/** * Authenticate the client making the request to a Phabricator user account. * * @param ConduitAPIRequest Request being executed. * @param dict Request metadata. * @return null|pair Null to indicate successful authentication, or * an error code and error message pair. */ private function authenticateUser(ConduitAPIRequest $api_request, array $metadata, $method) { $request = $this->getRequest(); if ($request->getUser()->getPHID()) { $request->validateCSRF(); return $this->validateAuthenticatedUser($api_request, $request->getUser()); } $auth_type = idx($metadata, 'auth.type'); if ($auth_type === ConduitClient::AUTH_ASYMMETRIC) { $host = idx($metadata, 'auth.host'); if (!$host) { return array('ERR-INVALID-AUTH', pht('Request is missing required "%s" parameter.', 'auth.host')); } // TODO: Validate that we are the host! $raw_key = idx($metadata, 'auth.key'); $public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($raw_key); $ssl_public_key = $public_key->toPKCS8(); // First, verify the signature. try { $protocol_data = $metadata; ConduitClient::verifySignature($method, $api_request->getAllParameters(), $protocol_data, $ssl_public_key); } catch (Exception $ex) { return array('ERR-INVALID-AUTH', pht('Signature verification failure. %s', $ex->getMessage())); } // If the signature is valid, find the user or device which is // associated with this public key. $stored_key = id(new PhabricatorAuthSSHKeyQuery())->setViewer(PhabricatorUser::getOmnipotentUser())->withKeys(array($public_key))->withIsActive(true)->executeOne(); if (!$stored_key) { return array('ERR-INVALID-AUTH', pht('No user or device is associated with that public key.')); } $object = $stored_key->getObject(); if ($object instanceof PhabricatorUser) { $user = $object; } else { if (!$stored_key->getIsTrusted()) { return array('ERR-INVALID-AUTH', pht('The key which signed this request is not trusted. Only ' . 'trusted keys can be used to sign API calls.')); } if (!PhabricatorEnv::isClusterRemoteAddress()) { return array('ERR-INVALID-AUTH', pht('This request originates from outside of the Phabricator ' . 'cluster address range. Requests signed with trusted ' . 'device keys must originate from within the cluster.')); } $user = PhabricatorUser::getOmnipotentUser(); // Flag this as an intracluster request. $api_request->setIsClusterRequest(true); } return $this->validateAuthenticatedUser($api_request, $user); } else { if ($auth_type === null) { // No specified authentication type, continue with other authentication // methods below. } else { return array('ERR-INVALID-AUTH', pht('Provided "%s" ("%s") is not recognized.', 'auth.type', $auth_type)); } } $token_string = idx($metadata, 'token'); if (strlen($token_string)) { if (strlen($token_string) != 32) { return array('ERR-INVALID-AUTH', pht('API token "%s" has the wrong length. API tokens should be ' . '32 characters long.', $token_string)); } $type = head(explode('-', $token_string)); $valid_types = PhabricatorConduitToken::getAllTokenTypes(); $valid_types = array_fuse($valid_types); if (empty($valid_types[$type])) { return array('ERR-INVALID-AUTH', pht('API token "%s" has the wrong format. API tokens should be ' . '32 characters long and begin with one of these prefixes: %s.', $token_string, implode(', ', $valid_types))); } $token = id(new PhabricatorConduitTokenQuery())->setViewer(PhabricatorUser::getOmnipotentUser())->withTokens(array($token_string))->withExpired(false)->executeOne(); if (!$token) { $token = id(new PhabricatorConduitTokenQuery())->setViewer(PhabricatorUser::getOmnipotentUser())->withTokens(array($token_string))->withExpired(true)->executeOne(); if ($token) { return array('ERR-INVALID-AUTH', pht('API token "%s" was previously valid, but has expired.', $token_string)); } else { return array('ERR-INVALID-AUTH', pht('API token "%s" is not valid.', $token_string)); } } // If this is a "cli-" token, it expires shortly after it is generated // by default. Once it is actually used, we extend its lifetime and make // it permanent. This allows stray tokens to get cleaned up automatically // if they aren't being used. if ($token->getTokenType() == PhabricatorConduitToken::TYPE_COMMANDLINE) { if ($token->getExpires()) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $token->setExpires(null); $token->save(); unset($unguarded); } } // If this is a "clr-" token, Phabricator must be configured in cluster // mode and the remote address must be a cluster node. if ($token->getTokenType() == PhabricatorConduitToken::TYPE_CLUSTER) { if (!PhabricatorEnv::isClusterRemoteAddress()) { return array('ERR-INVALID-AUTH', pht('This request originates from outside of the Phabricator ' . 'cluster address range. Requests signed with cluster API ' . 'tokens must originate from within the cluster.')); } // Flag this as an intracluster request. $api_request->setIsClusterRequest(true); } $user = $token->getObject(); if (!$user instanceof PhabricatorUser) { return array('ERR-INVALID-AUTH', pht('API token is not associated with a valid user.')); } return $this->validateAuthenticatedUser($api_request, $user); } $access_token = idx($metadata, 'access_token'); if ($access_token) { $token = id(new PhabricatorOAuthServerAccessToken())->loadOneWhere('token = %s', $access_token); if (!$token) { return array('ERR-INVALID-AUTH', pht('Access token does not exist.')); } $oauth_server = new PhabricatorOAuthServer(); $authorization = $oauth_server->authorizeToken($token); if (!$authorization) { return array('ERR-INVALID-AUTH', pht('Access token is invalid or expired.')); } $user = id(new PhabricatorPeopleQuery())->setViewer(PhabricatorUser::getOmnipotentUser())->withPHIDs(array($token->getUserPHID()))->executeOne(); if (!$user) { return array('ERR-INVALID-AUTH', 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.')); } $api_request->setOAuthToken($token); return $this->validateAuthenticatedUser($api_request, $user); } // For intracluster requests, use a public user if no authentication // information is provided. We could do this safely for any request, // but making the API fully public means there's no way to disable badly // behaved clients. if (PhabricatorEnv::isClusterRemoteAddress()) { if (PhabricatorEnv::getEnvConfig('policy.allow-public')) { $api_request->setIsClusterRequest(true); $user = new PhabricatorUser(); return $this->validateAuthenticatedUser($api_request, $user); } } // Handle sessionless auth. // TODO: This is super messy. // TODO: Remove this in favor of token-based auth. if (isset($metadata['authUser'])) { $user = id(new PhabricatorUser())->loadOneWhere('userName = %s', $metadata['authUser']); if (!$user) { return array('ERR-INVALID-AUTH', pht('Authentication is invalid.')); } $token = idx($metadata, 'authToken'); $signature = idx($metadata, 'authSignature'); $certificate = $user->getConduitCertificate(); $hash = sha1($token . $certificate); if (!phutil_hashes_are_identical($hash, $signature)) { return array('ERR-INVALID-AUTH', pht('Authentication is invalid.')); } return $this->validateAuthenticatedUser($api_request, $user); } // Handle session-based auth. // TODO: Remove this in favor of token-based auth. $session_key = idx($metadata, 'sessionKey'); if (!$session_key) { return array('ERR-INVALID-SESSION', pht('Session key is not present.')); } $user = id(new PhabricatorAuthSessionEngine())->loadUserForSession(PhabricatorAuthSession::TYPE_CONDUIT, $session_key); if (!$user) { return array('ERR-INVALID-SESSION', pht('Session key is invalid.')); } return $this->validateAuthenticatedUser($api_request, $user); }