Skip to content

Commit

Permalink
Allow protection of access tokens before device token unpaid
Browse files Browse the repository at this point in the history
This protection is necessary to prevent unwanted unpairing of
backend-created access tokens serving for device-inapp purchase
connection.

Commit covers scenario:

- Login with device token
- Verify inapp purchase
- Logout (device is still linked to the purchase thanks to backend-only
created access token during logout process)
- Login with other account

At this point, device should still be able to access the content,
but without the protection implemented here it wouldn't.

remp/crm#1494
  • Loading branch information
rootpd committed Sep 29, 2020
1 parent 9a8060d commit bb62ec0
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 1 deletion.
23 changes: 23 additions & 0 deletions src/DataProviders/AccessTokenDataProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Crm\GooglePlayBillingModule\DataProviders;

use Crm\GooglePlayBillingModule\GooglePlayBillingModule;
use Crm\UsersModule\DataProvider\AccessTokenDataProviderInterface;
use Nette\Database\Table\IRow;

class AccessTokenDataProvider implements AccessTokenDataProviderInterface
{
public function canUnpairDeviceToken(IRow $accessToken, IRow $deviceToken): bool
{
if ($accessToken->source === GooglePlayBillingModule::USER_SOURCE_APP) {
return false;
}
return true;
}

public function provide(array $params)
{
throw new \Exception('AccessTokenDataProvider does not provide generic method results');
}
}
9 changes: 9 additions & 0 deletions src/GooglePlayBillingModule.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Crm\ApiModule\Router\ApiIdentifier;
use Crm\ApiModule\Router\ApiRoute;
use Crm\ApplicationModule\CrmModule;
use Crm\ApplicationModule\DataProvider\DataProviderManager;
use Crm\ApplicationModule\SeederManager;
use Crm\GooglePlayBillingModule\Api\VerifyPurchaseApiHandler;
use Crm\UsersModule\Auth\UserTokenAuthorization;
Expand Down Expand Up @@ -64,4 +65,12 @@ public function registerEventHandlers(Emitter $emitter)
$this->getInstance(\Crm\GooglePlayBillingModule\Events\PairDeviceAccessTokensEventHandler::class)
);
}

public function registerDataProviders(DataProviderManager $dataProviderManager)
{
$dataProviderManager->registerDataProvider(
'users.dataprovider.access_tokens',
$this->getInstance(\Crm\GooglePlayBillingModule\DataProviders\AccessTokenDataProvider::class)
);
}
}
104 changes: 104 additions & 0 deletions src/Tests/AccessTokenDataProviderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

namespace Crm\GooglePlayBillingModule\Tests;

use Crm\ApplicationModule\DataProvider\DataProviderManager;
use Crm\ApplicationModule\Tests\DatabaseTestCase;
use Crm\GooglePlayBillingModule\GooglePlayBillingModule;
use Crm\UsersModule\Repositories\DeviceTokensRepository;
use Crm\UsersModule\Repository\AccessTokensRepository;
use Crm\UsersModule\Repository\UserMetaRepository;
use Crm\UsersModule\Repository\UsersRepository;
use Nette\Utils\Random;

class AccessTokenDataProviderTest extends DatabaseTestCase
{
/** @var AccessTokensRepository */
private $accessTokensRepository;

/** @var DeviceTokensRepository */
private $deviceTokensRepository;

/** @var UsersRepository */
private $usersRepository;

protected function requiredRepositories(): array
{
return [
UsersRepository::class,
UserMetaRepository::class,
DeviceTokensRepository::class,
AccessTokensRepository::class,
];
}

protected function requiredSeeders(): array
{
return [
];
}

public function setUp(): void
{
parent::setUp();

$this->accessTokensRepository = $this->getRepository(AccessTokensRepository::class);
$this->deviceTokensRepository = $this->getRepository(DeviceTokensRepository::class);
$this->usersRepository = $this->getRepository(UsersRepository::class);

$dataProviderManager = $this->inject(DataProviderManager::class);
$dataProviderManager->registerDataProvider(
'users.dataprovider.access_tokens',
$this->inject(\Crm\GooglePlayBillingModule\DataProviders\AccessTokenDataProvider::class)
);
}

public function testUnprotectedUnpairing()
{
$deviceToken = $this->deviceTokensRepository->generate('foo');

// pair users
$user1 = $this->getUser();
$accessToken1 = $this->accessTokensRepository->add($user1);
$this->accessTokensRepository->pairWithDeviceToken($accessToken1, $deviceToken);
$user2 = $this->getUser();
$accessToken2 = $this->accessTokensRepository->add($user2);
$this->accessTokensRepository->pairWithDeviceToken($accessToken2, $deviceToken);

// unpair token
$this->accessTokensRepository->unpairDeviceToken($deviceToken);

// regular unpairing should get rid of all access tokens linked to the device
$this->assertCount(
0,
$this->accessTokensRepository->findAllByDeviceToken($deviceToken)
);
}

public function testProtectedUnpairing()
{
$deviceToken = $this->deviceTokensRepository->generate('foo');

// pair users, protected second
$user1 = $this->getUser();
$accessToken1 = $this->accessTokensRepository->add($user1);
$this->accessTokensRepository->pairWithDeviceToken($accessToken1, $deviceToken);
$user2 = $this->getUser();
$accessToken2 = $this->accessTokensRepository->add($user2, 3, GooglePlayBillingModule::USER_SOURCE_APP);
$this->accessTokensRepository->pairWithDeviceToken($accessToken2, $deviceToken);

// unpair token
$this->accessTokensRepository->unpairDeviceToken($deviceToken);

// unpairing should get rid of the one access token without Google source
$this->assertCount(
1,
$this->accessTokensRepository->findAllByDeviceToken($deviceToken)
);
}

private function getUser()
{
return $this->usersRepository->add('user_' . Random::generate() . '@example.com', 'secret', '', '');
}
}
6 changes: 5 additions & 1 deletion src/api/VerifyPurchaseApiHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,11 @@ public function getUserFromSubscriptionResponse(SubscriptionResponse $subscripti
if (!$userId) {
return null;
}
return $this->usersRepository->find($userId);
$user = $this->usersRepository->find($userId);
if (!$user) {
return null;
}
return $user;
}

private function getUserIdFromSubscriptionResponse(SubscriptionResponse $subscriptionResponse): ?string
Expand Down
1 change: 1 addition & 0 deletions src/config/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ services:

- Crm\GooglePlayBillingModule\Api\DeveloperNotificationPushWebhookApiHandler
- Crm\GooglePlayBillingModule\Api\VerifyPurchaseApiHandler
- Crm\GooglePlayBillingModule\DataProviders\AccessTokenDataProvider
- Crm\GooglePlayBillingModule\Events\PairDeviceAccessTokensEventHandler
- Crm\GooglePlayBillingModule\Events\RemovedAccessTokenEventHandler
- Crm\GooglePlayBillingModule\Gateways\GooglePlayBilling
Expand Down

0 comments on commit bb62ec0

Please sign in to comment.