Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions src/Services/RemoteEventsFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace Bitrix24\SDK\Services;

use Bitrix24\SDK\Application\Contracts\Bitrix24Accounts\Entity\Bitrix24AccountInterface;
use Bitrix24\SDK\Application\Requests\Events\ApplicationLifeCycleEventsFactory;
use Bitrix24\SDK\Application\Requests\Events\OnApplicationInstall\OnApplicationInstall;
use Bitrix24\SDK\Core\Contracts\Events\EventInterface;
Expand Down Expand Up @@ -93,17 +94,14 @@ public function create(Request $request): EventInterface
}

/**
* @param EventInterface $event
* @param non-empty-string $applicationToken
* Check event security signature for incoming event
*
* All events MUST validate for application_token signature
* @see https://apidocs.bitrix24.com/api-reference/events/safe-event-handlers.html
* @throws WrongSecuritySignatureException
* @throws InvalidArgumentException
*/
public function validate(EventInterface $event, string $applicationToken): void
public function validate(Bitrix24AccountInterface $bitrix24Account, EventInterface $event): void
{
if ($applicationToken === '') {
throw new InvalidArgumentException('application token cannot be empty string');
}

if ($event instanceof OnApplicationInstall) {
// skip OnApplicationInstall event check because application_token is null
// first event in application lifecycle is OnApplicationInstall and this event contains application_token
Expand All @@ -113,18 +111,22 @@ public function validate(EventInterface $event, string $applicationToken): void
// check event security signature
// see https://apidocs.bitrix24.com/api-reference/events/safe-event-handlers.html
// all next events MUST validate for application_token signature
if ($applicationToken !== $event->getAuth()->application_token) {
if (!$bitrix24Account->isApplicationTokenValid($event->getAuth()->application_token)) {
$this->logger->warning('RemoteEventsFactory.validate.eventNotValidSignature', [
'eventCode' => $event->getEventCode(),
'storedApplicationToken' => $applicationToken,
'storedApplicationToken' => $bitrix24Account,
'eventApplicationToken' => $event->getAuth()->application_token,
'eventPayload' => $event->getEventPayload(),
'accountId' => $bitrix24Account->getId()->toRfc4122(),
'memberId' => $bitrix24Account->getMemberId(),
'domainUrl' => $bitrix24Account->getDomainUrl(),
]);

throw new WrongSecuritySignatureException(
sprintf(
'Wrong security signature for event %s',
$event->getEventCode()
'Wrong security signature for event %s processed by bitrix24 account %s',
$event->getEventCode(),
$bitrix24Account->getDomainUrl()
)
);
}
Expand Down
102 changes: 29 additions & 73 deletions tests/Unit/Services/RemoteEventsFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

namespace Bitrix24\SDK\Tests\Unit\Services;

use Bitrix24\SDK\Application\Contracts\Bitrix24Accounts\Entity\Bitrix24AccountInterface;
use Bitrix24\SDK\Application\Requests\Events\OnApplicationInstall\OnApplicationInstall;
use Bitrix24\SDK\Core\Contracts\Events\EventInterface;
use Bitrix24\SDK\Core\Exceptions\InvalidArgumentException;
Expand Down Expand Up @@ -217,16 +218,21 @@ public function testValidatePassesWithMatchingToken(): void
$request = $this->createRequest($rawRequest);
$event = $this->factory->create($request);

// Create mock account that validates the token as correct
$accountMock = $this->createMock(Bitrix24AccountInterface::class);
$accountMock->method('isApplicationTokenValid')
->with($applicationToken)
->willReturn(true);

// Should not throw any exception
$this->factory->validate($event, $applicationToken);
$this->factory->validate($accountMock, $event);
$this->assertTrue(true); // If we reach here, validation passed
}

#[Test]
#[TestDox('validate() should throw WrongSecuritySignatureException when tokens do not match')]
public function testValidateThrowsExceptionWithMismatchedToken(): void
{
$storedToken = 'stored_application_token_12345';
$eventToken = 'different_application_token_67890';

$rawRequest = $this->buildRawRequest([
Expand All @@ -252,41 +258,15 @@ public function testValidateThrowsExceptionWithMismatchedToken(): void
$request = $this->createRequest($rawRequest);
$event = $this->factory->create($request);

// Create mock account that validates the token as incorrect
$accountMock = $this->createMock(Bitrix24AccountInterface::class);
$accountMock->method('isApplicationTokenValid')
->with($eventToken)
->willReturn(false);

$this->expectException(WrongSecuritySignatureException::class);
$this->expectExceptionMessage('Wrong security signature for event ONCRMCONTACTADD');
$this->factory->validate($event, $storedToken);
}

#[Test]
#[TestDox('validate() should throw InvalidArgumentException when application token is empty')]
public function testValidateThrowsExceptionWithEmptyToken(): void
{
$rawRequest = $this->buildRawRequest([
'event' => 'ONCRMCONTACTADD',
'event_handler_id' => '196',
'data' => ['FIELDS' => ['ID' => '264442']],
'ts' => '1762089975',
'auth' => [
'access_token' => 'test_access_token',
'expires' => '1762093575',
'expires_in' => '3600',
'scope' => 'crm',
'domain' => 'test.bitrix24.com',
'server_endpoint' => 'https://oauth.bitrix.info/rest/',
'status' => 'L',
'client_endpoint' => 'https://test.bitrix24.com/rest/',
'member_id' => 'test_member_id',
'user_id' => '1',
'application_token' => 'test_app_token',
],
]);

$request = $this->createRequest($rawRequest);
$event = $this->factory->create($request);

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('application token cannot be empty string');
$this->factory->validate($event, ''); // @phpstan-ignore argument.type
$this->factory->validate($accountMock, $event);
}

#[Test]
Expand Down Expand Up @@ -319,44 +299,14 @@ public function testValidateSkipsCheckForApplicationInstallEvent(): void
$request = $this->createRequest($rawRequest);
$event = $this->factory->create($request);

// Should not throw exception even with different token
$this->factory->validate($event, 'completely_different_token');
$this->assertTrue(true); // If we reach here, validation was skipped
}
// Create mock account - should not be called for OnApplicationInstall
$accountMock = $this->createMock(Bitrix24AccountInterface::class);
$accountMock->expects($this->never())
->method('isApplicationTokenValid');

#[Test]
#[TestDox('validate() should still require non-empty token for OnApplicationInstall events')]
public function testValidateRequiresNonEmptyTokenForApplicationInstallEvent(): void
{
$rawRequest = $this->buildRawRequest([
'event' => 'ONAPPINSTALL',
'event_handler_id' => '1',
'data' => [
'VERSION' => '1',
'LANGUAGE_ID' => 'en',
],
'ts' => '1762089975',
'auth' => [
'access_token' => 'test_access_token',
'expires' => '1762093575',
'expires_in' => '3600',
'scope' => 'crm,placement,user_brief',
'domain' => 'test.bitrix24.com',
'server_endpoint' => 'https://oauth.bitrix.info/rest/',
'status' => 'L',
'client_endpoint' => 'https://test.bitrix24.com/rest/',
'member_id' => 'test_member_id',
'user_id' => '1',
'application_token' => 'event_app_token',
],
]);

$request = $this->createRequest($rawRequest);
$event = $this->factory->create($request);

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('application token cannot be empty string');
$this->factory->validate($event, ''); // @phpstan-ignore argument.type
// Should not throw exception and should not check token
$this->factory->validate($accountMock, $event);
$this->assertTrue(true); // If we reach here, validation was skipped
}

#[Test]
Expand Down Expand Up @@ -387,8 +337,14 @@ public function testValidateWithVariousEventTypes(string $eventCode, string $app
$request = $this->createRequest($rawRequest);
$event = $this->factory->create($request);

// Create mock account that validates the token as correct
$accountMock = $this->createMock(Bitrix24AccountInterface::class);
$accountMock->method('isApplicationTokenValid')
->with($applicationToken)
->willReturn(true);

// Should not throw exception
$this->factory->validate($event, $applicationToken);
$this->factory->validate($accountMock, $event);
$this->assertTrue(true);
}

Expand Down