Views: 10865
Last Modified: 17.08.2020

Isolated Bitrix24 Self-hosted

How applications work on Bitrix24 Self-hosted without external network connection.

Company security policy can have all kinds limitations for internal and external network resources due to which Bitrix24 REST apps not always can "reach" Bitrix24 Self-hosted or external cloud services. Nevertheless, there is a solution that allows developing applications using standard REST API for Bitrix24 even in such cases.

Note: the solution described below (exclusion from server oauth.bitrix.info authentication process) is recommended only in extreme cases, because you will have to solve app secure use and authentication management issues.

Requesting external resources

Application REST has 3 cases when Bitrix24 portal sends an external query:

1 - Authentication validator
2 - Authentication provider
3 - Event provider

Let's review methods that allow avoiding requests to external resources.

Note: The example presents situation when a single specific application must be initiated but standard data communication chain avoided.

Authentication validator

Create your own event handler

<?php
namespace Demo\AuthProvider;

class AuthSimple
{
	const AUTH_TYPE = 'demo_simple';

	const AUTH_PARAM_NAME = 'secret_word';
	const AUTH_PARAM_VALUE = 'MySuperSecurePassword123456';

	public static function onRestCheckAuth(array $query, $scope, &$res)
	{
		if(array_key_exists(static::AUTH_PARAM_NAME, $query))
		{
			$res = array('error' => 'INVALID_CREDENTIALS', 'error_description' => 'Invalid request credentials');

			return false;
		}

		return null;
	}
}

Validator receives the app's full request data. When request doesn't contain parameter secret_word, it returns return null, meaning "request not identified". When request has this parameter, the handler checks its value. If the value is available but does not match to the saved one, response is the following: "yes, it's my request, but value is incorrect". When the value is correct, it communicates user ID, the list of available scopes, indicates which parameters must be deleted before sending request to the handler, as well as authentication type ID and which methods have limitations for authentication types.

After the handler calls a REST module's method authenticating the user on this hit. Returns true on success.

Event provider

Inherit the original event provider class and indicate that it implements event provider interface. Re-define only single function send in our class. The example below demonstrates how to call an external handler when you need to execute direct handler http-request ($http->post(...)) instead of querying an external queue.

<?php
namespace Demo\AuthProvider;

use Bitrix\Rest\Event\ProviderInterface;
use Bitrix\Rest\Event\ProviderOAuth;
use Bitrix\Rest\Event\Sender;

class EventProvider extends ProviderOAuth implements ProviderInterface
{
	public static function onEventManagerInitialize()
	{
		Sender::setProvider(static::instance());
	}

	public function send(array $queryData)
	{
		$http = new \Bitrix\Main\Web\HttpClient();
		foreach($queryData as $key => $item)
		{
			if($this->checkItem($item))
			{
				$http->post($item['query']['QUERY_URL'], $item['query']['QUERY_DATA']);
				unset($queryData[$key]);
			}
		}

		if(count($queryData) > 0)
		{
			parent::send(array_values($queryData));
		}
	}

	protected function checkItem(array $item)
	{
		return AuthProvider::instance()->checkClient($item['client_id']);
	}
}

The handler checks the complete event call array to determine if this app and handler are created by us. When this app is not created by us, the request will be forwarded to parent class; otherwise, executes post request to the app on the same hit. After that, register new event provider as the main provider and proceed with event calling.

Authentication provider

The most complex part.

The principle is the same: inherit an original provider class and create your own provide class implementing alternative authentication provider interface we need.

<?php
namespace Demo\AuthProvider;


use Bitrix\Main\Context;
use Bitrix\Main\NotImplementedException;
use Bitrix\Main\ObjectNotFoundException;
use Bitrix\Main\Security\Random;
use Bitrix\Rest\Application;
use Bitrix\Rest\AppTable;
use Bitrix\Rest\AuthProviderInterface;
use Bitrix\Rest\OAuth\Provider;
use Bitrix\Rest\RestException;

class AuthProvider extends Provider implements AuthProviderInterface
{
	const TOKEN_TTL = 3600;
	const TOKEN_PREFIX = 'demo.';

	protected $applicationList = array();

	/**
	 * @var AuthProvider
	 */
	protected static $instance = null;

	/**
	 * @var AuthStorageInterface
	 */
	protected $storage;

	/**
	 * @return AuthProvider
	 */
	public static function instance()
	{
		if(static::$instance === null)
		{
			static::$instance = new static();
		}

		return static::$instance;
	}

	public static function onApplicationManagerInitialize()
	{
		Application::setAuthProvider(static::instance());
	}

	public function get($clientId, $scope, $additionalParams, $userId)
	{
		if(!$this->checkClient($clientId))
		{
			return parent::get($clientId, $scope, $additionalParams, $userId);
		}

		if($userId > 0)
		{
			$applicationData = AppTable::getByClientId($clientId);

			if($applicationData)
			{
				$authResult = array(
					'access_token' => $this->generateToken(),
					'user_id' => $userId,
					'client_id' => $clientId,
					'expires' => time() + static::TOKEN_TTL,
					'expires_in' => static::TOKEN_TTL,
					'scope' => $applicationData['SCOPE'],
					'domain' => Context::getCurrent()->getServer()->getHttpHost(),
					'status' => AppTable::STATUS_LOCAL,
					'client_endpoint' => \CRestUtil::getEndpoint(),
					'member_id' => \CRestUtil::getMemberId(),
				);

				$this->store($authResult);

				return $authResult;
			}
			else
			{
				$authResult = array('error' => RestException::ERROR_OAUTH, 'Application not installed');
			}

			return $authResult;
		}

		return false;
	}

	public function authorizeClient($clientId, $userId, $state = '')
	{
		if(!$this->checkClient($clientId))
		{
			return parent::authorizeClient($clientId, $userId, $state);
		}

		throw new NotImplementedException('Full OAuth authorization is not implemented in this demo');
	}

	public function checkClient($clientId)
	{
		return in_array($clientId, $this->applicationList);
	}

	protected function store(array $authResult)
	{
		$this->getStorage()->store($authResult);
	}

	public function checkToken($token)
	{
		return substr($token, 0, strlen(static::TOKEN_PREFIX)) === static::TOKEN_PREFIX;
	}

	protected function generateToken()
	{
		return static::TOKEN_PREFIX.Random::getString(32);
	}

	/**
	 * @return AuthStorageInterface
	 * @throws ObjectNotFoundException
	 */
	public function getStorage()
	{
		if($this->storage === null)
		{
			throw new ObjectNotFoundException('No token storage set. Use '.__CLASS__.'::instance()->setStorage().');
		}

		return $this->storage;
	}

	/**
	 * @param AuthStorageInterface $storage
	 * @return AuthProvider
	 */
	public function setStorage(AuthStorageInterface $storage)
	{
		$this->storage = $storage;

		return $this;
	}

	/**
	 * @param string $clientId
	 * @return AuthProvider
	 */
	public function addApplication($clientId)
	{
		$this->applicationList[] = $clientId;

		return $this;
	}
}

Main method is the method get designed for issuing authorization for an application. This method receives an app, which has "requested" authorization and checks if this application is the same app that we wanted to "exclude" from the standard authentication mechanism. Then, receives the app data and a structure similar to the structure retrieved by all applications from standard Bitrix24 OAuth-server. The array contains:

  • access_token - created token.
  • user_id - user to be authenticated.
  • client_id - application. Please note, any token lifetime period can be specified, not only specific hour, used by default in standard authorization.
  • expires - token expiration date.
  • scope - required scopes.
  • service data.

This data is saved on the portal. Then, returns the generated structure.

Additional methods

Register new provider as the current authentication provider:

\Bitrix\Rest\Application::setAuthProvider(
      Demo\AuthProvider\AuthProvider::instance()
);

Now, when executing this request with authentication token and calling method \Bitrix\Rest\AppInfo, we return the app data within current Bitrix24:

Create your own authentication validator.

<?php
namespace Demo\AuthProvider;

use Bitrix\Rest\OAuth\Auth;

class AuthFull extends Auth
{
	protected static function check($accessToken)
	{
		if(!AuthProvider::instance()->checkToken($accessToken))
		{
			return parent::check($accessToken);
		}

		$authResult = AuthProvider::instance()->getStorage()->restore($accessToken);

		if($authResult === false)
		{
			$authResult = array('error' => 'invalid_token', 'error_description' => 'Token expired or invalid');
		}

		return $authResult;
	}

}

Inherit new validator from original one and redefine its check function, designed for checking accessToken. On success, restores the application from storage. Then, register the event handler:

\Bitrix\Main\EventHandler::getInstance()
      ->registerEventHadler(
             "rest", "onRestCheckAuth", "demo.authprovider", "\\Demo\\AuthProvider\\AuthFull", "onRestCheckAuth", 90
         );

The last parameter is sorting. You need to finalize embedding before original handler is triggered.

Now, execute request with this authorization token and call the method \Bitrix\Rest\AppInfo to get app data for this portal.

Update the event provider to pass authentication data to the application and into event handlers:

if($item['additional']['sendAuth'])
				{
					$item['query']['QUERY_DATA']['auth'] = AuthProvider::instance()->get(
						$item['client_id'],
						'',
						$item['auth'],
						$item['auth'][AuthFull::PARAM_LOCAL_USER]
					);
				}
Full event provider code

The event handler gets full data structure required for operation.

Notes on performance. Code added to the event provider in our example above is executed directly in the event handler. The code has post request for the third-party server. Subsequently, when a third-party server is operating slowly, Bitrix24 account will also be operating slowly. When, for example, mass-volume operation is performed, such as data import to CRM, this code can significantly slow down Bitrix24 account's operation.

There are two ways to avoid this performance issue:

  • Queue building. Instead of sending a post request, collect data in a table and use additional processes to electively retrieve data from it. Essentially - a custom queue processing.
  • Use mechanism of offline events



Courses developed by Bitrix24