From 1468f6565d30e716a70fecf8ad82fa9a3b10312f Mon Sep 17 00:00:00 2001 From: David Hollis Date: Mon, 23 Apr 2018 17:48:28 -0400 Subject: [PATCH 1/5] Change to use ShareFile OAuth2 client --- composer.json | 1 + src/Client.php | 105 +++++++++++++++++++------------------------------ 2 files changed, 42 insertions(+), 64 deletions(-) diff --git a/composer.json b/composer.json index 334efb9..fe785f6 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ ], "require": { "php": "^7.0", + "slacker775/oauth2-sharefile": "^1.0", "guzzlehttp/guzzle": "^6.2" }, "require-dev": { diff --git a/src/Client.php b/src/Client.php index 1ce9188..2fe3f0d 100644 --- a/src/Client.php +++ b/src/Client.php @@ -6,9 +6,9 @@ use GuzzleHttp\Psr7; use GuzzleHttp\HandlerStack; use GuzzleHttp\Handler\MockHandler; -use GuzzleHttp\Client as GuzzleClient; use GuzzleHttp\Exception\ClientException; use Kapersoft\Sharefile\Exceptions\BadRequest; +use Slacker775\OAuth2\Client\Provider\ShareFile as AuthProvider; /** * Class Client. @@ -28,11 +28,22 @@ class Client public $token; /** - * Guzzle Client. * - * @var \GuzzleHttp\Client + * @var AbstractProvider */ - public $client; + protected $authProvider; + + /** + * + * @var AccessToken + */ + protected $accessToken; + + /** + * + * @var array + */ + protected $options; /** * Thumbnail size. @@ -67,64 +78,17 @@ class Client */ public function __construct(string $hostname, string $client_id, string $client_secret, string $username, string $password, $handler = null) { - $response = $this->authenticate($hostname, $client_id, $client_secret, $username, $password, $handler); - - if (! isset($response['access_token']) || ! isset($response['subdomain'])) { - throw new Exception("Incorrect response from Authentication: 'access_token' or 'subdomain' is missing."); - } - - $this->token = $response; - $this->client = new GuzzleClient( - [ - 'handler' => $handler, - 'headers' => [ - 'Authorization' => "Bearer {$this->token['access_token']}", - ], - ] - ); - } - - /** - * ShareFile authentication using username/password. - * - * @param string $hostname ShareFile hostname - * @param string $client_id OAuth2 client_id - * @param string $client_secret OAuth2 client_secret - * @param string $username ShareFile username - * @param string $password ShareFile password - * @param MockHandler|HandlerStack $handler Guzzle Handler - * - * @throws Exception - * - * @return array - */ - protected function authenticate(string $hostname, string $client_id, string $client_secret, string $username, string $password, $handler = null):array - { - $uri = "https://{$hostname}/oauth/token"; - - $parameters = [ - 'grant_type' => 'password', - 'client_id' => $client_id, - 'client_secret' => $client_secret, - 'username' => $username, - 'password' => $password, + $this->authProvider = new AuthProvider([ + 'clientId' => $client_id, + 'clientSecret' => $client_secret + ]); + + $this->options = [ + 'username' => $username, + 'password' => $password, + 'baseUrl' => $hostname, ]; - try { - $client = new GuzzleClient(['handler' => $handler]); - $response = $client->post( - $uri, - ['form_params' => $parameters] - ); - } catch (ClientException $exception) { - throw $exception; - } - - if ($response->getStatusCode() == '200') { - return json_decode($response->getBody(), true); - } else { - throw new Exception('Authentication error', $response->getStatusCode()); - } } /** @@ -368,9 +332,10 @@ public function uploadFileStandard(string $filename, string $folderId, bool $unz { $chunkUri = $this->getChunkUri('standard', $filename, $folderId, $unzip, $overwrite, $notify); - $response = $this->client->request( + $request = $this->authProvider->getAuthenticatedRequest( 'POST', $chunkUri['ChunkUri'], + $this->accessToken, [ 'multipart' => [ [ @@ -380,6 +345,7 @@ public function uploadFileStandard(string $filename, string $folderId, bool $unz ], ] ); + $response = $this->authProvider->getResponse($request); return (string) $response->getBody(); } @@ -522,7 +488,7 @@ public function getItemAccessControls(string $itemId, string $userId = ''):array */ protected function buildUri(string $endpoint): string { - return "https://{$this->token['subdomain']}.sf-api.com/sf/v3/{$endpoint}"; + return "https://{$this->accessToken->getValues()['subdomain']}.sf-api.com/sf/v3/{$endpoint}"; } /** @@ -538,11 +504,20 @@ protected function buildUri(string $endpoint): string */ protected function request(string $method, string $endpoint, $json = null) { + if (is_null($this->accessToken)) { + $this->accessToken = $this->authProvider->getAccessToken('password', [ + 'username' => $this->options['username'], + 'password' => $this->options['password'], + 'baseUrl' => $this->options['baseUrl'] + ]); + } + $uri = $this->buildUri($endpoint); $options = $json != null ? ['json' => $json] : []; try { - $response = $this->client->request($method, $uri, $options); + $request = $this->authProvider->getAuthenticatedRequest($method, $uri, $this->accessToken, $options); + $response = $this->authProvider->getResponse($request); } catch (ClientException $exception) { throw $this->determineException($exception); } @@ -612,9 +587,10 @@ protected function delete(string $endpoint) */ protected function uploadChunk($uri, $data) { - $response = $this->client->request( + $request = $this->authProvider->getAuthenticatedRequest( 'POST', $uri, + $this->accessToken, [ 'headers' => [ 'Content-Length' => strlen($data), @@ -623,6 +599,7 @@ protected function uploadChunk($uri, $data) 'body' => $data, ] ); + $response = $this->authProvider->getResponse($request); return (string) $response->getBody(); } From 2b4fe642fcd72e18ba72cdbd2e7436543dc3b08c Mon Sep 17 00:00:00 2001 From: David Hollis Date: Tue, 24 Sep 2019 10:59:07 -0400 Subject: [PATCH 2/5] Update to use oauth2 library and token storage to reduce auth round-trips --- .gitignore | 4 ++ composer.json | 14 +++--- src/Client.php | 116 +++++++++++++++++++++++++++++++++++++------------ 3 files changed, 101 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index db78b71..17b20b7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,7 @@ composer.lock vendor phpunit.xml .phpunit-watcher.yml +.buildpath +.settings +.project +test.php diff --git a/composer.json b/composer.json index fe785f6..0298b4c 100644 --- a/composer.json +++ b/composer.json @@ -21,21 +21,22 @@ "require": { "php": "^7.0", "slacker775/oauth2-sharefile": "^1.0", - "guzzlehttp/guzzle": "^6.2" + "guzzlehttp/guzzle": "^6.2", + "slacker775/oauth2-tokenstorage": "^1.0" }, "require-dev": { "larapack/dd": "1.*", - "mikey179/vfsStream": "^1.6", - "phpunit/phpunit": "6.4.x-dev" + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": "^8.0" }, "autoload": { "psr-4": { - "Kapersoft\\Sharefile\\": "src" + "Kapersoft\\Sharefile\\": "src/" } }, "autoload-dev": { "psr-4": { - "Kapersoft\\Sharefile\\Test\\": "tests" + "Kapersoft\\Sharefile\\Test\\": "tests/" } }, "scripts": { @@ -43,5 +44,8 @@ }, "config": { "sort-packages": true + }, + "suggest": { + "league/flysystem": "Utilize Flysystem for OAuth Token Storage" } } diff --git a/src/Client.php b/src/Client.php index 2fe3f0d..f67c714 100644 --- a/src/Client.php +++ b/src/Client.php @@ -1,14 +1,19 @@ tokenRepository = $tokenRepository; + + $client = new HttpClient([ + 'handler' => $handler + ]); + $this->authProvider = new AuthProvider([ 'clientId' => $client_id, 'clientSecret' => $client_secret + ], [ + 'httpClient' => $client ]); $this->options = [ 'username' => $username, 'password' => $password, - 'baseUrl' => $hostname, + 'baseUrl' => $hostname ]; - } /** @@ -98,11 +124,16 @@ public function __construct(string $hostname, string $client_id, string $client_ * * @return array */ - public function getUser(string $userId = ''):array + public function getUser(string $userId = '') : array { return $this->get("Users({$userId})"); } + public function updateUser(string $userId, array $data) : array + { + return $this->patch("Users({$userId})", $data); + } + /** * Create a folder. * @@ -113,7 +144,7 @@ public function getUser(string $userId = ''):array * * @return array */ - public function createFolder(string $parentId, string $name, string $description = '', bool $overwrite = false):array + public function createFolder(string $parentId, string $name, string $description = '', bool $overwrite = false) : array { $parameters = $this->buildHttpQuery( [ @@ -138,7 +169,7 @@ public function createFolder(string $parentId, string $name, string $description * * @return array */ - public function getItemById(string $itemId, bool $getChildren = false):array + public function getItemById(string $itemId, bool $getChildren = false) : array { $parameters = $getChildren === true ? '$expand=Children' : ''; @@ -153,7 +184,7 @@ public function getItemById(string $itemId, bool $getChildren = false):array * * @return array */ - public function getItemByPath(string $path, string $itemId = ''):array + public function getItemByPath(string $path, string $itemId = '') : array { if (empty($itemId)) { return $this->get("Items/ByPath?Path={$path}"); @@ -163,13 +194,13 @@ public function getItemByPath(string $path, string $itemId = ''):array } /** - * Get breadcrumps of an item. + * Get breadcrumbs of an item. * * @param string $itemId Item Id * * @return array */ - public function getItemBreadcrumps(string $itemId):array + public function getItemBreadcrumbs(string $itemId) : array { return $this->get("Items({$itemId})/Breadcrumbs"); } @@ -183,7 +214,7 @@ public function getItemBreadcrumps(string $itemId):array * * @return array */ - public function copyItem(string $targetId, string $itemId, bool $overwrite = false):array + public function copyItem(string $targetId, string $itemId, bool $overwrite = false) : array { $parameters = $this->buildHttpQuery( [ @@ -205,7 +236,7 @@ public function copyItem(string $targetId, string $itemId, bool $overwrite = fal * * @return array */ - public function updateItem(string $itemId, array $data, bool $forceSync = true, bool $notify = true):array + public function updateItem(string $itemId, array $data, bool $forceSync = true, bool $notify = true) : array { $parameters = $this->buildHttpQuery( [ @@ -226,7 +257,7 @@ public function updateItem(string $itemId, array $data, bool $forceSync = true, * * @return string */ - public function deleteItem(string $itemId, bool $singleversion = false, bool $forceSync = false):string + public function deleteItem(string $itemId, bool $singleversion = false, bool $forceSync = false) : string { $parameters = $this->buildHttpQuery( [ @@ -479,6 +510,41 @@ public function getItemAccessControls(string $itemId, string $userId = ''):array } } + protected function getAccessToken(): AccessToken + { + $tokenId = sprintf('sf-%s', $this->options['username']); + + if ($this->accessToken === null) { + if ($this->tokenRepository !== null) { + try { + $this->accessToken = $this->tokenRepository->loadToken($tokenId); + } catch(TokenNotFoundException $e) {} + } + + if ($this->accessToken === null) { + $this->accessToken = $this->authProvider->getAccessToken('password', [ + 'username' => $this->options['username'], + 'password' => $this->options['password'], + 'baseUrl' => $this->options['baseUrl'] + ]); + + if ($this->tokenRepository !== null) { + $this->tokenRepository->storeToken($this->accessToken, $tokenId); + } + } + } + + if ($this->accessToken->hasExpired() === true) { + $this->accessToken = $this->authProvider->getAccessToken('refresh_token', [ + 'refresh_token' => $this->accessToken->getRefreshToken() + ]); + if ($this->tokenRepository !== null) { + $this->tokenRepository->storeAccessToken($tokenId, $this->accessToken); + } + } + return $this->accessToken; + } + /** * Build API uri. * @@ -504,19 +570,13 @@ protected function buildUri(string $endpoint): string */ protected function request(string $method, string $endpoint, $json = null) { - if (is_null($this->accessToken)) { - $this->accessToken = $this->authProvider->getAccessToken('password', [ - 'username' => $this->options['username'], - 'password' => $this->options['password'], - 'baseUrl' => $this->options['baseUrl'] - ]); - } + $accessToken = $this->getAccessToken(); $uri = $this->buildUri($endpoint); $options = $json != null ? ['json' => $json] : []; try { - $request = $this->authProvider->getAuthenticatedRequest($method, $uri, $this->accessToken, $options); + $request = $this->authProvider->getAuthenticatedRequest($method, $uri, $accessToken, $options); $response = $this->authProvider->getResponse($request); } catch (ClientException $exception) { throw $this->determineException($exception); From dc8ef017285a8f1b4bbe46176397ef1040093fca Mon Sep 17 00:00:00 2001 From: David Hollis Date: Tue, 24 Sep 2019 16:09:54 -0400 Subject: [PATCH 3/5] Update PSR4 autoload namespace --- composer.json | 99 +++++++++++++++++++++++++-------------------------- 1 file changed, 49 insertions(+), 50 deletions(-) diff --git a/composer.json b/composer.json index 0298b4c..46a788e 100644 --- a/composer.json +++ b/composer.json @@ -1,51 +1,50 @@ { - "name": "kapersoft/sharefile-api", - "description": "A minimal implementation of ShareFile Api", - "keywords": [ - "kapersoft", - "sharefile-api", - "sharefile", - "api", - "php" - ], - "homepage": "https://github.com/kapersoft/sharefile-api", - "license": "MIT", - "authors": [ - { - "name": "Jan Willem Kaper", - "email": "kapersoft@gmail.com", - "homepage": "https://kapersoft.com", - "role": "Developer" - } - ], - "require": { - "php": "^7.0", - "slacker775/oauth2-sharefile": "^1.0", - "guzzlehttp/guzzle": "^6.2", - "slacker775/oauth2-tokenstorage": "^1.0" - }, - "require-dev": { - "larapack/dd": "1.*", - "mikey179/vfsstream": "^1.6", - "phpunit/phpunit": "^8.0" - }, - "autoload": { - "psr-4": { - "Kapersoft\\Sharefile\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "Kapersoft\\Sharefile\\Test\\": "tests/" - } - }, - "scripts": { - "test": "vendor/bin/phpunit" - }, - "config": { - "sort-packages": true - }, - "suggest": { - "league/flysystem": "Utilize Flysystem for OAuth Token Storage" - } -} + "name" : "kapersoft/sharefile-api", + "description" : "A minimal implementation of ShareFile Api", + "keywords" : [ + "kapersoft", + "sharefile-api", + "sharefile", + "api", + "php" + ], + "homepage" : "https://github.com/kapersoft/sharefile-api", + "license" : "MIT", + "authors" : [{ + "name" : "Jan Willem Kaper", + "email" : "kapersoft@gmail.com", + "homepage" : "https://kapersoft.com", + "role" : "Developer" + } + ], + "require" : { + "php" : "^7.0", + "slacker775/oauth2-sharefile" : "^1.0", + "guzzlehttp/guzzle" : "^6.2", + "slacker775/oauth2-tokenstorage" : "^1.0" + }, + "require-dev" : { + "larapack/dd" : "1.*", + "mikey179/vfsstream" : "^1.6", + "phpunit/phpunit" : "^8.0" + }, + "autoload" : { + "psr-4" : { + "Kapersoft\\ShareFile\\" : "src/" + } + }, + "autoload-dev" : { + "psr-4" : { + "Kapersoft\\Sharefile\\Test\\" : "tests/" + } + }, + "scripts" : { + "test" : "vendor/bin/phpunit" + }, + "config" : { + "sort-packages" : true + }, + "suggest" : { + "league/flysystem" : "Utilize Flysystem for OAuth Token Storage" + } +} \ No newline at end of file From 8e67351ad57f0d6f79631f67a96fc992c3c09dfc Mon Sep 17 00:00:00 2001 From: David Hollis Date: Tue, 24 Sep 2019 17:00:57 -0400 Subject: [PATCH 4/5] Fix namespace for BadRequest exception --- src/Client.php | 2 +- src/Exceptions/BadRequest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Client.php b/src/Client.php index f67c714..beb85d5 100644 --- a/src/Client.php +++ b/src/Client.php @@ -8,7 +8,7 @@ use GuzzleHttp\HandlerStack; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\Exception\ClientException; -use Kapersoft\Sharefile\Exceptions\BadRequest; +use Kapersoft\ShareFile\Exceptions\BadRequest; use Slacker775\OAuth2\Client\Provider\ShareFile as AuthProvider; use League\OAuth2\Client\Provider\AbstractProvider; use League\OAuth2\Client\Token\AccessToken; diff --git a/src/Exceptions/BadRequest.php b/src/Exceptions/BadRequest.php index 2bf67d8..7147b92 100644 --- a/src/Exceptions/BadRequest.php +++ b/src/Exceptions/BadRequest.php @@ -1,6 +1,6 @@ Date: Fri, 27 Sep 2019 11:27:36 -0400 Subject: [PATCH 5/5] Cleanup unit tests a bit and convert to work w/ oauth handling --- composer.json | 4 +- src/Client.php | 4 +- tests/TestClient.php | 109 +++++++------------------------------------ 3 files changed, 21 insertions(+), 96 deletions(-) diff --git a/composer.json b/composer.json index 46a788e..777ce3b 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ "require-dev" : { "larapack/dd" : "1.*", "mikey179/vfsstream" : "^1.6", - "phpunit/phpunit" : "^8.0" + "phpunit/phpunit" : "^6.4" }, "autoload" : { "psr-4" : { @@ -47,4 +47,4 @@ "suggest" : { "league/flysystem" : "Utilize Flysystem for OAuth Token Storage" } -} \ No newline at end of file +} diff --git a/src/Client.php b/src/Client.php index beb85d5..376ce1d 100644 --- a/src/Client.php +++ b/src/Client.php @@ -433,7 +433,7 @@ public function uploadFileStreamed($stream, string $folderId, string $filename = 'index' => $index, 'byteOffset' => $index * $chunkSize, 'hash' => md5($data), - 'filehash' => Psr7\hash(Psr7\stream_for($stream), 'md5'), + 'filehash' => \GuzzleHttp\Psr7\hash(\GuzzleHttp\Psr7\stream_for($stream), 'md5'), 'finish' => true, ] ); @@ -510,7 +510,7 @@ public function getItemAccessControls(string $itemId, string $userId = ''):array } } - protected function getAccessToken(): AccessToken + public function getAccessToken(): AccessToken { $tokenId = sprintf('sf-%s', $this->options['username']); diff --git a/tests/TestClient.php b/tests/TestClient.php index 56d7dca..8083ef5 100644 --- a/tests/TestClient.php +++ b/tests/TestClient.php @@ -7,11 +7,11 @@ use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; use org\bovigo\vfs\vfsStream; -use Kapersoft\Sharefile\Client; -use PHPUnit\Framework\TestCase; +use Kapersoft\ShareFile\Client; use GuzzleHttp\Handler\MockHandler; use GuzzleHttp\Exception\ClientException; use org\bovigo\vfs\content\LargeFileContent; +use PHPUnit\Framework\TestCase; /** * Class TestClient. @@ -59,7 +59,7 @@ public function setUp() public function it_can_be_instantiated() // @codingStandardsIgnoreLine { $mockHandler = new MockHandler( - [new Response(200, [], json_encode(['access_token' => 'my_access_code', 'subdomain' => 'subdomain']))] + [new Response(200, [], json_encode(['access_token' => 'my_access_code', 'subdomain' => 'subdomain', 'expires' => time() + 60]))] ); $client = new Client( @@ -72,89 +72,7 @@ public function it_can_be_instantiated() // @codingStandardsIgnoreLine ); $this->assertInstanceOf(Client::class, $client); - $this->assertEquals('my_access_code', $client->token['access_token']); - } - - /** - * Test for it_can_throw_an_exception. - * - * @test - * - * @return void - */ - public function it_can_throw_an_exception() // @codingStandardsIgnoreLine - { - $mockHandler = new MockHandler( - [new Response(400)] - ); - - $this->expectException(\Exception::class); - $this->expectExceptionMessage('Authentication error'); - $this->expectExceptionCode(400); - - $client = new Client( - 'hostname', - 'client_id', - 'secret', - 'username', - 'password', - $mockHandler - ); - } - - /** - * Test for it_can_handle_an_incorrect_authentication_response. - * - * @test - * - * @return void - */ - public function it_can_handle_an_incorrect_authentication_response() // @codingStandardsIgnoreLine - { - $mockHandler = new MockHandler( - [new Response(200, [], json_encode([]))] - ); - - $this->expectException(\Exception::class); - $this->expectExceptionMessage("Incorrect response from Authentication: 'access_token' or 'subdomain' is missing."); - - $client = new Client( - 'hostname', - 'client_id', - 'secret', - 'username', - 'password', - $mockHandler - ); - } - - /** - * Test for it_can_throw_an_client_exception. - * - * @test - * - * @return void - */ - public function it_can_throw_an_client_exception() // @codingStandardsIgnoreLine - { - $mockHandler = new MockHandler( - [ - new ClientException('Could not resolve host: hostname', new Request('POST', 'hostname'), new response(404)), - ] - ); - - $this->expectException(ClientException::class); - $this->expectExceptionCode(404); - $this->expectExceptionMessage('Could not resolve host: hostname'); - - $client = new Client( - 'hostname', - 'client_id', - 'secret', - 'username', - 'password', - $mockHandler - ); + $this->assertEquals('my_access_code', $client->getAccessToken()->getToken()); } /** @@ -247,7 +165,7 @@ public function it_can_get_an_item_with_children() // @codingStandardsIgnoreLine $expectedResponse = ['odata.type' => 'odata.metadata', 'odata.count' => '2', 'value' => []]; $mockClient = $this->getMockClient($expectedResponse); - $response = $mockClient->getItemBreadcrumps(Client::FOLDER_HOME); + $response = $mockClient->getItemBreadcrumbs(Client::FOLDER_HOME); $this->assertSame('GET', (string) $this->getLastRequest()->getMethod()); $this->assertSame('https://subdomain.sf-api.com/sf/v3/Items(home)/Breadcrumbs', (string) $this->getLastRequest()->getUri()); @@ -274,13 +192,13 @@ public function it_can_get_item_by_path() // @codingStandardsIgnoreLine } /** - * Test for it_can_get_item_breadcrumps. + * Test for it_can_get_item_breadcrumbs. * * @test * * @return void */ - public function it_can_get_item_breadcrumps() // @codingStandardsIgnoreLine + public function it_can_get_item_breadcrumbs() // @codingStandardsIgnoreLine { $expectedResponse = ['odata.type' => 'ShareFile.Api.Models.Folder', 'Id' => 'top', 'Children' => '']; $mockClient = $this->getMockClient($expectedResponse); @@ -442,7 +360,7 @@ public function it_can_upload_an_item_using_single_http_post() // @codingStandar // Create response $expectedResponse = 'OK'; $mockResponse = [ - new Response(200, [], json_encode(['access_token' => 'access_code', 'subdomain' => 'subdomain'])), + new Response(200, [], json_encode(['access_token' => 'access_code', 'subdomain' => 'subdomain', 'expires' => time() + 60])), new Response(200, [], json_encode(['ChunkUri' => 'https://storage-eu-202.sharefile.com/upload.aspx?uploadid=my_upload_id'])), new Response(200, [], $expectedResponse), ]; @@ -484,7 +402,7 @@ public function it_can_upload_an_item_using_multiple_http_posts() // @codingStan // Create response $expectedResponse = 'fo66e8f5-3aa3-405b-8129-f9a749dd4e99'; $mockResponse = [ - new Response(200, [], json_encode(['access_token' => 'access_code', 'subdomain' => 'subdomain'])), + new Response(200, [], json_encode(['access_token' => 'access_code', 'subdomain' => 'subdomain', 'expires' => time() + 60])), new Response(200, [], json_encode(['ChunkUri' => 'https://storage-eu-202.sharefile.com/upload.aspx?uploadid=my_upload_id'])), new Response(200, [], 'true'), new Response(200, [], $expectedResponse), @@ -648,7 +566,14 @@ private function getMockClient($responseBody, array $responseHeaders = []): Clie } $mockResponse = [ - new Response(200, [], json_encode(['access_token' => 'access_code', 'subdomain' => 'subdomain'])), + new Response(200, [], json_encode([ + 'access_token' => 'access_code', + 'subdomain' => 'subdomain', + 'expires' => time() + 60, + 'refresh_token' => 'refresh_code', + 'token_type' => 'bearer', + 'appcp' => 'sharefile.com' + ])), new Response(200, $responseHeaders, $responseBody), ];