From b529e2b83b6107580a567be33dad3df18ce4e9b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9F=D0=B0=D0=B2=D0=B5=D0=BB?= Date: Wed, 14 Oct 2020 14:55:27 +0300 Subject: [PATCH] oauth code exchange --- src/Builder/ContainerBuilder.php | 10 ++ .../Exception/OAuthTokenFetcherException.php | 29 +++ src/Component/OAuthTokenFetcher.php | 170 ++++++++++++++++++ src/Component/ServiceLocator.php | 9 + src/Factory/OAuthTokenFetcherFactory.php | 98 ++++++++++ .../ParametrizedFactoryInterface.php | 34 ++++ src/Interfaces/TopClientInterface.php | 6 + src/Model/Request/OAuthTokenFetchRequest.php | 92 ++++++++++ .../Response/OAuthTokenFetcherResponse.php | 124 +++++++++++++ src/TopClient/TopClient.php | 9 + .../Tests/Component/OAuthTokenFetcherTest.php | 70 ++++++++ .../RetailCrm/Tests/TopClient/ClientTest.php | 3 - 12 files changed, 651 insertions(+), 3 deletions(-) create mode 100644 src/Component/Exception/OAuthTokenFetcherException.php create mode 100644 src/Component/OAuthTokenFetcher.php create mode 100644 src/Factory/OAuthTokenFetcherFactory.php create mode 100644 src/Interfaces/ParametrizedFactoryInterface.php create mode 100644 src/Model/Request/OAuthTokenFetchRequest.php create mode 100644 src/Model/Response/OAuthTokenFetcherResponse.php create mode 100644 tests/RetailCrm/Tests/Component/OAuthTokenFetcherTest.php diff --git a/src/Builder/ContainerBuilder.php b/src/Builder/ContainerBuilder.php index c3b279b..ecf97e6 100644 --- a/src/Builder/ContainerBuilder.php +++ b/src/Builder/ContainerBuilder.php @@ -28,6 +28,7 @@ use RetailCrm\Component\DependencyInjection\Container; use RetailCrm\Component\Environment; use RetailCrm\Component\ServiceLocator; use RetailCrm\Factory\FileItemFactory; +use RetailCrm\Factory\OAuthTokenFetcherFactory; use RetailCrm\Factory\ProductSchemaStorageFactory; use RetailCrm\Factory\SerializerFactory; use RetailCrm\Factory\TopRequestFactory; @@ -220,6 +221,15 @@ class ContainerBuilder implements BuilderInterface $container->set(Constants::SERIALIZER, function (ContainerInterface $container) { return SerializerFactory::withContainer($container)->create(); }); + $container->set(OAuthTokenFetcherFactory::class, function (ContainerInterface $container) { + return new OAuthTokenFetcherFactory( + $container->get(Constants::SERIALIZER), + $container->get(StreamFactoryInterface::class), + $container->get(RequestFactoryInterface::class), + $container->get(UriFactoryInterface::class), + $container->get(Constants::HTTP_CLIENT) + ); + }); $container->set(FileItemFactoryInterface::class, function (ContainerInterface $container) { return new FileItemFactory($container->get(StreamFactoryInterface::class)); }); diff --git a/src/Component/Exception/OAuthTokenFetcherException.php b/src/Component/Exception/OAuthTokenFetcherException.php new file mode 100644 index 0000000..dcaaac1 --- /dev/null +++ b/src/Component/Exception/OAuthTokenFetcherException.php @@ -0,0 +1,29 @@ + + * @license http://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see http://help.retailcrm.ru + */ + +namespace RetailCrm\Component\Exception; + +use Exception; + +/** + * Class OAuthTokenFetcherException + * + * @category OAuthTokenFetcherException + * @package RetailCrm\Component\Exception + * @author RetailDriver LLC + * @license https://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see https://help.retailcrm.ru + */ +class OAuthTokenFetcherException extends Exception +{ +} diff --git a/src/Component/OAuthTokenFetcher.php b/src/Component/OAuthTokenFetcher.php new file mode 100644 index 0000000..f6d6626 --- /dev/null +++ b/src/Component/OAuthTokenFetcher.php @@ -0,0 +1,170 @@ + + * @license http://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see http://help.retailcrm.ru + */ + +namespace RetailCrm\Component; + +use Exception; +use JMS\Serializer\SerializerInterface; +use Psr\Http\Client\ClientExceptionInterface; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Message\StreamInterface; +use Psr\Http\Message\UriFactoryInterface; +use RetailCrm\Component\Exception\OAuthTokenFetcherException; +use RetailCrm\Interfaces\AppDataInterface; +use RetailCrm\Model\Request\OAuthTokenFetchRequest; +use RetailCrm\Model\Response\OAuthTokenFetcherResponse; + +/** + * Class OAuthTokenFetcher + * + * @category OAuthTokenFetcher + * @package RetailCrm\Component + * @author RetailDriver LLC + * @license https://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see https://help.retailcrm.ru + */ +class OAuthTokenFetcher +{ + /** + * @var SerializerInterface|\JMS\Serializer\Serializer $serializer + */ + private $serializer; + + /** + * @var StreamFactoryInterface $streamFactory + */ + private $streamFactory; + + /** + * @var \Psr\Http\Message\RequestFactoryInterface $requestFactory + */ + private $requestFactory; + + /** + * @var \Psr\Http\Message\UriFactoryInterface $uriFactory + */ + private $uriFactory; + + /** + * @var ClientInterface $client + */ + private $client; + + /** + * @var \RetailCrm\Interfaces\AppDataInterface $appData + */ + private $appData; + + /** + * OAuthTokenFetcher constructor. + * + * @param \JMS\Serializer\SerializerInterface $serializer + * @param \Psr\Http\Message\StreamFactoryInterface $streamFactory + * @param \Psr\Http\Message\RequestFactoryInterface $requestFactory + * @param \Psr\Http\Message\UriFactoryInterface $uriFactory + * @param \Psr\Http\Client\ClientInterface $client + */ + public function __construct( + SerializerInterface $serializer, + StreamFactoryInterface $streamFactory, + RequestFactoryInterface $requestFactory, + UriFactoryInterface $uriFactory, + ClientInterface $client + ) { + $this->serializer = $serializer; + $this->streamFactory = $streamFactory; + $this->requestFactory = $requestFactory; + $this->uriFactory = $uriFactory; + $this->client = $client; + } + + /** + * @param \RetailCrm\Interfaces\AppDataInterface $appData + * + * @return OAuthTokenFetcher + */ + public function setAppData(AppDataInterface $appData): OAuthTokenFetcher + { + $this->appData = $appData; + return $this; + } + + /** + * @param string $code + * @param string $state + * + * @return \RetailCrm\Model\Response\OAuthTokenFetcherResponse + * @throws \RetailCrm\Component\Exception\OAuthTokenFetcherException + */ + public function fetchToken(string $code, string $state = ''): OAuthTokenFetcherResponse + { + $request = new OAuthTokenFetchRequest(); + $request->clientId = $this->appData->getAppKey(); + $request->clientSecret = $this->appData->getAppSecret(); + $request->redirectUri = $this->appData->getRedirectUri(); + $request->code = $code; + + if ('' !== $state) { + $request->state = $state; + } + + $requestData = $this->serializer->toArray($request); + $postData = http_build_query($requestData); + $request = null; + $response = null; + + try { + $request = $this->requestFactory + ->createRequest( + 'POST', + $this->uriFactory->createUri('https://oauth.aliexpress.com/token') + )->withBody($this->streamFactory->createStream($postData)) + ->withHeader('content-type', 'application/x-www-form-urlencoded; charset=UTF-8'); + } catch (Exception $exception) { + throw new OAuthTokenFetcherException( + sprintf('Cannot create request: %s', $exception->getMessage()), + $exception + ); + } + + try { + $response = $this->client->sendRequest($request); + } catch (ClientExceptionInterface $exception) { + throw new OAuthTokenFetcherException( + sprintf('Cannot send request: %s', $exception->getMessage()), + $exception + ); + } + + return $this->serializer->deserialize( + self::getBodyContents($response->getBody()), + OAuthTokenFetcherResponse::class, + 'json' + ); + } + + /** + * Returns body stream data (it should work like that in order to keep compatibility with some implementations). + * + * @param \Psr\Http\Message\StreamInterface $stream + * + * @return string + */ + protected static function getBodyContents(StreamInterface $stream): string + { + return $stream->isSeekable() ? $stream->__toString() : $stream->getContents(); + } +} diff --git a/src/Component/ServiceLocator.php b/src/Component/ServiceLocator.php index 330017b..71955af 100644 --- a/src/Component/ServiceLocator.php +++ b/src/Component/ServiceLocator.php @@ -13,6 +13,7 @@ namespace RetailCrm\Component; use RetailCrm\Factory\FileItemFactory; +use RetailCrm\Factory\OAuthTokenFetcherFactory; use RetailCrm\Interfaces\ContainerAwareInterface; use RetailCrm\Interfaces\FileItemFactoryInterface; use RetailCrm\Traits\ContainerAwareTrait; @@ -31,6 +32,14 @@ class ServiceLocator implements ContainerAwareInterface { use ContainerAwareTrait; + /** + * @return \RetailCrm\Factory\OAuthTokenFetcherFactory + */ + public function getOAuthTokenFetcherFactory(): OAuthTokenFetcherFactory + { + return $this->getContainer()->get(OAuthTokenFetcherFactory::class); + } + /** * @return \RetailCrm\Interfaces\FileItemFactoryInterface */ diff --git a/src/Factory/OAuthTokenFetcherFactory.php b/src/Factory/OAuthTokenFetcherFactory.php new file mode 100644 index 0000000..3883a6d --- /dev/null +++ b/src/Factory/OAuthTokenFetcherFactory.php @@ -0,0 +1,98 @@ + + * @license http://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see http://help.retailcrm.ru + */ + +namespace RetailCrm\Factory; + +use JMS\Serializer\SerializerInterface; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Message\UriFactoryInterface; +use RetailCrm\Component\OAuthTokenFetcher; +use RetailCrm\Interfaces\ParametrizedFactoryInterface; + +/** + * Class OAuthTokenFetcherFactory + * + * @category OAuthTokenFetcherFactory + * @package RetailCrm\Factory + * @author RetailDriver LLC + * @license https://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see https://help.retailcrm.ru + */ +class OAuthTokenFetcherFactory implements ParametrizedFactoryInterface +{ + /** + * @var SerializerInterface|\JMS\Serializer\Serializer $serializer + */ + private $serializer; + + /** + * @var StreamFactoryInterface $streamFactory + */ + private $streamFactory; + + /** + * @var \Psr\Http\Message\RequestFactoryInterface $requestFactory + */ + private $requestFactory; + + /** + * @var \Psr\Http\Message\UriFactoryInterface $uriFactory + */ + private $uriFactory; + + /** + * @var ClientInterface $client + */ + private $client; + + /** + * OAuthTokenFetcherFactory constructor. + * + * @param \JMS\Serializer\SerializerInterface $serializer + * @param \Psr\Http\Message\StreamFactoryInterface $streamFactory + * @param \Psr\Http\Message\RequestFactoryInterface $requestFactory + * @param \Psr\Http\Message\UriFactoryInterface $uriFactory + * @param \Psr\Http\Client\ClientInterface $client + */ + public function __construct( + SerializerInterface $serializer, + StreamFactoryInterface $streamFactory, + RequestFactoryInterface $requestFactory, + UriFactoryInterface $uriFactory, + ClientInterface $client + ) { + $this->serializer = $serializer; + $this->streamFactory = $streamFactory; + $this->requestFactory = $requestFactory; + $this->uriFactory = $uriFactory; + $this->client = $client; + } + + /** + * @param \RetailCrm\Interfaces\AppDataInterface $params + * + * @return \RetailCrm\Component\OAuthTokenFetcher + */ + public function create($params): OAuthTokenFetcher + { + return (new OAuthTokenFetcher( + $this->serializer, + $this->streamFactory, + $this->requestFactory, + $this->uriFactory, + $this->client + ))->setAppData($params); + } +} diff --git a/src/Interfaces/ParametrizedFactoryInterface.php b/src/Interfaces/ParametrizedFactoryInterface.php new file mode 100644 index 0000000..2209630 --- /dev/null +++ b/src/Interfaces/ParametrizedFactoryInterface.php @@ -0,0 +1,34 @@ + + * @license MIT https://mit-license.org + * @link http://retailcrm.ru + * @see http://help.retailcrm.ru + */ + +namespace RetailCrm\Interfaces; + +/** + * Interface ParametrizedFactoryInterface + * + * @category ParametrizedFactoryInterface + * @package RetailCrm\Interfaces + * @author RetailDriver LLC + * @license MIT https://mit-license.org + * @link http://retailcrm.ru + * @see https://help.retailcrm.ru + */ +interface ParametrizedFactoryInterface +{ + /** + * @param mixed $params + * + * @return object + */ + public function create($params); +} diff --git a/src/Interfaces/TopClientInterface.php b/src/Interfaces/TopClientInterface.php index b6f4d5c..0f8f2f6 100644 --- a/src/Interfaces/TopClientInterface.php +++ b/src/Interfaces/TopClientInterface.php @@ -12,6 +12,7 @@ */ namespace RetailCrm\Interfaces; +use RetailCrm\Component\OAuthTokenFetcher; use RetailCrm\Component\ServiceLocator; use RetailCrm\Model\Request\BaseRequest; use RetailCrm\Model\Response\TopResponseInterface; @@ -48,6 +49,11 @@ interface TopClientInterface */ public function getAuthorizationUriBuilder(string $state = ''): BuilderInterface; + /** + * @return \RetailCrm\Component\OAuthTokenFetcher + */ + public function getTokenFetcher(): OAuthTokenFetcher; + /** * Send TOP request * diff --git a/src/Model/Request/OAuthTokenFetchRequest.php b/src/Model/Request/OAuthTokenFetchRequest.php new file mode 100644 index 0000000..58f1f73 --- /dev/null +++ b/src/Model/Request/OAuthTokenFetchRequest.php @@ -0,0 +1,92 @@ + + * @license http://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see http://help.retailcrm.ru + */ + +namespace RetailCrm\Model\Request; + +use JMS\Serializer\Annotation as JMS; + +/** + * Class OAuthTokenFetchRequest + * + * @category OAuthTokenFetchRequest + * @package RetailCrm\Model\Request + * @author RetailDriver LLC + * @license https://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see https://help.retailcrm.ru + */ +class OAuthTokenFetchRequest +{ + /** + * @var string $clientId + * + * @JMS\Type("string") + * @JMS\SerializedName("client_id") + */ + public $clientId; + + /** + * @var string $clientSecret + * + * @JMS\Type("string") + * @JMS\SerializedName("client_secret") + */ + public $clientSecret; + + /** + * @var string $grantType + * + * @JMS\Type("string") + * @JMS\SerializedName("grant_type") + */ + public $grantType = 'authorization_code'; + + /** + * @var string $code + * + * @JMS\Type("string") + * @JMS\SerializedName("code") + */ + public $code; + + /** + * @var string $redirectUri + * + * @JMS\Type("string") + * @JMS\SerializedName("redirect_uri") + */ + public $redirectUri; + + /** + * @var string $sp + * + * @JMS\Type("string") + * @JMS\SerializedName("sp") + */ + public $sp = 'ae'; + + /** + * @var string $state + * + * @JMS\Type("string") + * @JMS\SerializedName("state") + */ + public $state; + + /** + * @var string $view + * + * @JMS\Type("string") + * @JMS\SerializedName("view") + */ + public $view = 'web'; +} diff --git a/src/Model/Response/OAuthTokenFetcherResponse.php b/src/Model/Response/OAuthTokenFetcherResponse.php new file mode 100644 index 0000000..53f1d82 --- /dev/null +++ b/src/Model/Response/OAuthTokenFetcherResponse.php @@ -0,0 +1,124 @@ + + * @license http://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see http://help.retailcrm.ru + */ + +namespace RetailCrm\Model\Response; + +use JMS\Serializer\Annotation as JMS; + +/** + * Class OAuthTokenFetcherResponse + * + * @category OAuthTokenFetcherResponse + * @package RetailCrm\Model\Response + * @author RetailDriver LLC + * @license https://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see https://help.retailcrm.ru + */ +class OAuthTokenFetcherResponse +{ + /** + * @var string $accessToken + * + * @JMS\Type("string") + * @JMS\SerializedName("access_token") + */ + public $accessToken; + + /** + * @var string $refreshToken + * + * @JMS\Type("string") + * @JMS\SerializedName("refresh_token") + */ + public $refreshToken; + + /** + * @var int $w1Valid + * + * @JMS\Type("int") + * @JMS\SerializedName("w1_valid") + */ + public $w1Valid; + + /** + * @var int $refreshTokenValidTime + * + * @JMS\Type("int") + * @JMS\SerializedName("refresh_token_valid_time") + */ + public $refreshTokenValidTime; + + /** + * @var int $w2Valid + * + * @JMS\Type("int") + * @JMS\SerializedName("w2_valid") + */ + public $w2Valid; + + /** + * @var string $userId + * + * @JMS\Type("string") + * @JMS\SerializedName("user_id") + */ + public $userId; + + /** + * @var int $expireTime + * + * @JMS\Type("int") + * @JMS\SerializedName("expire_time") + */ + public $expireTime; + + /** + * @var int $r2Valid + * + * @JMS\Type("int") + * @JMS\SerializedName("r2_valid") + */ + public $r2Valid; + + /** + * @var string $locale + * + * @JMS\Type("string") + * @JMS\SerializedName("locale") + */ + public $locale; + + /** + * @var int $r1Valid + * + * @JMS\Type("int") + * @JMS\SerializedName("r1_valid") + */ + public $r1Valid; + + /** + * @var string $sp + * + * @JMS\Type("string") + * @JMS\SerializedName("sp") + */ + public $sp; + + /** + * @var string $userNick + * + * @JMS\Type("string") + * @JMS\SerializedName("user_nick") + */ + public $userNick; +} diff --git a/src/TopClient/TopClient.php b/src/TopClient/TopClient.php index fb883c8..e15f5a0 100644 --- a/src/TopClient/TopClient.php +++ b/src/TopClient/TopClient.php @@ -22,6 +22,7 @@ use RetailCrm\Builder\AuthorizationUriBuilder; use RetailCrm\Component\Environment; use RetailCrm\Component\Exception\TopApiException; use RetailCrm\Component\Exception\TopClientException; +use RetailCrm\Component\OAuthTokenFetcher; use RetailCrm\Component\ServiceLocator; use RetailCrm\Component\Storage\ProductSchemaStorage; use RetailCrm\Factory\ProductSchemaStorageFactory; @@ -211,6 +212,14 @@ class TopClient implements TopClientInterface return new AuthorizationUriBuilder($this->appData->getAppKey(), $this->appData->getRedirectUri(), $state); } + /** + * @return \RetailCrm\Component\OAuthTokenFetcher + */ + public function getTokenFetcher(): OAuthTokenFetcher + { + return $this->getServiceLocator()->getOAuthTokenFetcherFactory()->create($this->appData); + } + /** * @return \RetailCrm\Component\Storage\ProductSchemaStorage */ diff --git a/tests/RetailCrm/Tests/Component/OAuthTokenFetcherTest.php b/tests/RetailCrm/Tests/Component/OAuthTokenFetcherTest.php new file mode 100644 index 0000000..a5345bb --- /dev/null +++ b/tests/RetailCrm/Tests/Component/OAuthTokenFetcherTest.php @@ -0,0 +1,70 @@ + + * @license http://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see http://help.retailcrm.ru + */ + +namespace RetailCrm\Tests\Component; + +use RetailCrm\Builder\TopClientBuilder; +use RetailCrm\Test\RequestMatcher; +use RetailCrm\Test\TestCase; + +/** + * Class OAuthTokenFetcherTest + * + * @category OAuthTokenFetcherTest + * @package RetailCrm\Tests\Component + * @author RetailDriver LLC + * @license https://retailcrm.ru Proprietary + * @link http://retailcrm.ru + * @see https://help.retailcrm.ru + */ +class OAuthTokenFetcherTest extends TestCase +{ + public function testFetchToken() + { + $jsonResponse = <<<'EOF' +{ + "access_token": "accessToken", + "refresh_token": "refreshToken", + "w1_valid": 1, + "refresh_token_valid_time": 1602676186888, + "w2_valid": 1, + "user_id": "0000000000", + "expire_time": 1, + "r2_valid": 1, + "locale": "zh_CN", + "r1_valid": 1, + "sp": "ae", + "user_nick": "ru0000000000" +} +EOF; + $mock = self::getMockClient(); + $mock->on( + RequestMatcher::createMatcher('oauth.aliexpress.com') + ->setPath('/token') + ->setOptionalPostFields([ + 'code' => 'oauthCode', + 'state' => '{"accountId":5,"token":"login-5f86e5579dad92"}' + ]), + $this->responseJson(200, $jsonResponse) + ); + $tokenFetcher = TopClientBuilder::create() + ->setContainer($this->getContainer($mock)) + ->setAppData($this->getEnvAppData()) + ->setAuthenticator($this->getEnvTokenAuthenticator()) + ->build() + ->getTokenFetcher(); + $response = $tokenFetcher->fetchToken('oauthCode', '{"accountId":5,"token":"login-5f86e5579dad92"}'); + + self::assertEquals('accessToken', $response->accessToken); + self::assertEquals('refreshToken', $response->refreshToken); + } +} diff --git a/tests/RetailCrm/Tests/TopClient/ClientTest.php b/tests/RetailCrm/Tests/TopClient/ClientTest.php index e84a259..d83f161 100644 --- a/tests/RetailCrm/Tests/TopClient/ClientTest.php +++ b/tests/RetailCrm/Tests/TopClient/ClientTest.php @@ -16,9 +16,6 @@ use DateTime; use Http\Message\RequestMatcher\CallbackRequestMatcher; use Psr\Http\Message\RequestInterface; use RetailCrm\Builder\TopClientBuilder; -use RetailCrm\Component\Constants; -use RetailCrm\Component\Logger\FileLogger; -use RetailCrm\Component\Logger\StdoutLogger; use RetailCrm\Model\Entity\CategoryInfo; use RetailCrm\Model\Enum\DropshippingAreas; use RetailCrm\Model\Enum\FeedOperationTypes;