From a73343e85450a32cdbcbe68432766700901229f7 Mon Sep 17 00:00:00 2001 From: Ben Lofo Date: Wed, 27 Sep 2023 11:19:53 -0400 Subject: [PATCH] Add Client Credentials Flow as Authentication Method --- README.md | 21 +- .../Authentications/ClientCredentialsSpec.php | 859 ++++++++++++++++++ .../Authentications/ClientCredentials.php | 74 ++ src/Omniphx/Forrest/Client.php | 2 + .../Interfaces/ClientCredentialsInterface.php | 7 + .../Forrest/Providers/BaseServiceProvider.php | 17 + src/config/config.php | 4 +- 7 files changed, 979 insertions(+), 5 deletions(-) create mode 100644 spec/Omniphx/Forrest/Authentications/ClientCredentialsSpec.php create mode 100644 src/Omniphx/Forrest/Authentications/ClientCredentials.php create mode 100644 src/Omniphx/Forrest/Interfaces/ClientCredentialsInterface.php diff --git a/README.md b/README.md index 42bc136..785b3db 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,22 @@ Route::get('/callback', function() With the Username Password flow, you can directly authenticate with the `Forrest::authenticate()` method. -> To use this authentication you must add your username, and password to the config file. Security token might need to be ammended to your password unless your IP address is whitelisted. +> To use this authentication you must add your username, and password to the config file. Security token might need to be amended to your password unless your IP address is whitelisted. + +```php +Route::get('/authenticate', function() +{ + Forrest::authenticate(); + return Redirect::to('/'); +}); +``` + + +#### Client Credentials authentication flow + +With the Client Credentials flow, you can directly authenticate with the `Forrest::authenticate()` method. + +> Using this authentication method only requires your consumer secret and key. Your Salesforce Connected app must also have the "Client Credentials Flow" Enabled in its settings. ```php Route::get('/authenticate', function() @@ -135,7 +150,7 @@ Route::get('/authenticate', function() 4. Update your config file with values for `loginURL`, `username`, and `password`. With the Username Password SOAP flow, you can directly authenticate with the `Forrest::authenticate()` method. -> To use this authentication you can add your username, and password to the config file. Security token might need to be ammended to your password unless your IP address is whitelisted. +> To use this authentication you can add your username, and password to the config file. Security token might need to be amended to your password unless your IP address is whitelisted. ```php Route::get('/authenticate', function() @@ -147,7 +162,7 @@ Route::get('/authenticate', function() If your application requires logging in to salesforce as different users, you can alternatively pass in the login url, username, and password to the `Forrest::authenticateUser()` method. -> Security token might need to be ammended to your password unless your IP address is whitelisted. +> Security token might need to be amended to your password unless your IP address is whitelisted. ```php Route::Post('/authenticate', function(Request $request) diff --git a/spec/Omniphx/Forrest/Authentications/ClientCredentialsSpec.php b/spec/Omniphx/Forrest/Authentications/ClientCredentialsSpec.php new file mode 100644 index 0000000..e15f57c --- /dev/null +++ b/spec/Omniphx/Forrest/Authentications/ClientCredentialsSpec.php @@ -0,0 +1,859 @@ + "Spring 15", + "url" => "/services/data/v33.0", + "version" => "33.0" + ], + [ + "label" => "Summer 15", + "url" => "/services/data/v34.0", + "version" => "34.0" + ], + [ + "label" => "Winter 16", + "url" => "/services/data/v35.0", + "version" => "35.0" + ] + ]; + + protected $authenticationJSON = '{ + "access_token": "00Do0000000secret", + "id": "https://login.salesforce.com/id/00Do0000000xxxxx/005o0000000xxxxx", + "instance_url": "https://na17.salesforce.com", + "issued_at": "1447000236011", + "signature": "secretsig", + "token_type": "Bearer" + }'; + + protected $responseXML = ' + + I\'m Mr. Meseeks, look at me! + Get 2 strokes off Gary\'s golf swing + Have you tried squring your shoulders, Gary? + '; + + protected $responseNone = 'A non formatted response'; + + protected $token = [ + 'access_token' => '00Do0000000secret', + 'instance_url' => 'https://na17.salesforce.com', + 'id' => 'https://login.salesforce.com/id/00Do0000000xxxxx/005o0000000xxxxx', + 'token_type' => 'Bearer', + 'issued_at' => '1447000236011', + 'signature' => 'secretsig']; + + protected $decodedResponse = ['foo' => 'bar']; + + protected $settings = [ + 'authenticationFlow' => 'ClientCredentials', + 'credentials' => [ + 'consumerKey' => 'testingClientId', + 'consumerSecret' => 'testingClientSecret', + 'callbackURI' => 'callbackURL', + 'loginURL' => 'https://login.salesforce.com', + 'username' => 'user@email.com', + 'password' => 'mypassword', + ], + 'parameters' => [ + 'display' => 'popup', + 'immediate' => 'false', + 'state' => '', + 'scope' => '', + ], + 'instanceURL' => '', + 'authRedirect' => 'redirectURL', + 'version' => '30.0', + 'defaults' => [ + 'method' => 'get', + 'format' => 'json', + 'compression' => false, + 'compressionType' => 'gzip', + ], + 'language' => 'en_US', + ]; + + public function let( + ClientInterface $mockedHttpClient, + EncryptorInterface $mockedEncryptor, + EventInterface $mockedEvent, + InputInterface $mockedInput, + RedirectInterface $mockedRedirect, + ResponseInterface $mockedResponse, + RepositoryInterface $mockedInstanceURLRepo, + RepositoryInterface $mockedRefreshTokenRepo, + ResourceRepositoryInterface $mockedResourceRepo, + RepositoryInterface $mockedStateRepo, + RepositoryInterface $mockedTokenRepo, + RepositoryInterface $mockedVersionRepo, + FormatterInterface $mockedFormatter) + { + $this->beConstructedWith( + $mockedHttpClient, + $mockedEncryptor, + $mockedEvent, + $mockedInput, + $mockedRedirect, + $mockedInstanceURLRepo, + $mockedRefreshTokenRepo, + $mockedResourceRepo, + $mockedStateRepo, + $mockedTokenRepo, + $mockedVersionRepo, + $mockedFormatter, + $this->settings); + + $mockedInstanceURLRepo->get()->willReturn('https://instance.salesforce.com'); + + $mockedResourceRepo->get(Argument::any())->willReturn('/services/data/v30.0/resource'); + $mockedResourceRepo->put(Argument::any())->willReturn(null); + + $mockedTokenRepo->get()->willReturn($this->token); + $mockedTokenRepo->put($this->token)->willReturn(null); + + $mockedFormatter->setBody(Argument::any())->willReturn(null); + $mockedFormatter->setHeaders()->willReturn([ + 'Authorization' => 'Oauth accessToken', + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ]); + $mockedFormatter->getDefaultMIMEType()->willReturn('application/json'); + + $mockedVersionRepo->get()->willReturn(['url' => '/resources']); + + $mockedFormatter->formatResponse($mockedResponse)->willReturn(['foo' => 'bar']); + } + + public function it_is_initializable() + { + $this->shouldHaveType('Omniphx\Forrest\Authentications\ClientCredentials'); + } + + public function it_should_authenticate( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + ResponseInterface $mockedVersionRepo, + FormatterInterface $mockedFormatter, + ResponseInterface $versionResponse, + Stream $body) + { + $mockedHttpClient->request( + 'post', + 'url/services/oauth2/token', + ['form_params' => [ + 'grant_type' => 'client_credentials', + 'client_id' => 'testingClientId', + 'client_secret' => 'testingClientSecret', + ]]) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedHttpClient->request( + 'get', + 'https://instance.salesforce.com/resources', + ['headers' => [ + 'Authorization' => 'Oauth accessToken', + 'Accept' => 'application/json', + 'Content-Type' => 'application/json' + ]]) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $body->getContents()->shouldBeCalled()->willReturn($this->authenticationJSON); + $mockedResponse->getBody()->shouldBeCalled()->willReturn($body); + + $mockedHttpClient->request( + 'get', + 'https://instance.salesforce.com/services/data', + ['headers' => [ + 'Authorization' => 'Oauth accessToken', + 'Accept' => 'application/json', + 'Content-Type' => 'application/json' + ]]) + ->shouldBeCalled() + ->willReturn($versionResponse); + + $mockedFormatter->formatResponse($versionResponse)->shouldBeCalled()->willReturn($this->versionArray); + + $mockedVersionRepo->has()->willReturn(false); + $mockedVersionRepo->put(["label" => "Winter 16", "url" => "/services/data/v35.0", "version" => "35.0"])->shouldBeCalled(); + + $this->authenticate('url')->shouldReturn(null); + } + + public function it_should_refresh(ClientInterface $mockedHttpClient, ResponseInterface $mockedResponse, Stream $body) + { + $mockedHttpClient->request( + 'post', + 'https://login.salesforce.com/services/oauth2/token', + ['form_params' => [ + 'grant_type' => 'client_credentials', + 'client_id' => 'testingClientId', + 'client_secret' => 'testingClientSecret', + ]]) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $body->getContents()->shouldBeCalled()->willReturn($this->authenticationJSON); + $mockedResponse->getBody()->shouldBeCalled()->willReturn($body); + + $this->refresh()->shouldReturn(null); + } + + public function it_should_return_the_request( + ClientInterface $mockedHttpClient, + RequestInterface $mockedRequest, + ResponseInterface $mockedResponse) + { + $mockedHttpClient->request( + 'get', + 'url', + ['headers' => [ + 'Authorization' => 'Oauth accessToken', + 'Accept' => 'application/json', + 'Content-Type' => 'application/json']]) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $this->request('url', ['key' => 'value'])->shouldReturn(['foo' => 'bar']); + } + + public function it_should_refresh_the_token_if_token_expired_exception_is_thrown( + ClientInterface $mockedHttpClient, + RequestInterface $mockedRequest, + ResponseInterface $mockedResponse, + Stream $body) + { + $failedRequest = new Request('GET', 'fakeurl'); + $failedResponse = new Response(401); + $requestException = new RequestException('Salesforce token has expired', $failedRequest, $failedResponse); + + //First request throws an exception + $mockedHttpClient->request('get', 'url', ['headers' => ['Authorization' => 'Oauth accessToken', 'Accept' => 'application/json', 'Content-Type' => 'application/json']])->shouldBeCalled(1)->willThrow($requestException); + + //Authenticates with refresh method + $mockedHttpClient->request( + 'post', + 'https://login.salesforce.com/services/oauth2/token', + ['form_params' => [ + 'grant_type' => 'client_credentials', + 'client_id' => 'testingClientId', + 'client_secret' => 'testingClientSecret', + ]])->shouldBeCalled()->willReturn($mockedResponse); + + $body->getContents()->shouldBeCalled()->willReturn($this->authenticationJSON); + $mockedResponse->getBody()->shouldBeCalled(1)->willReturn($body); + + //This might seem counter-intuitive. We are throwing an exception with the send() method, but we can't stop it. Since we are calling the send() method twice, the behavior is correct for it to throw an exception. Actual behavior would never throw the exception, it would return a response. + $tokenException = new TokenExpiredException( + 'Salesforce token has expired', + $requestException); + + //Here we will handle a 401 exception and convert it to a TokenExpiredException + $this->shouldThrow($tokenException)->duringRequest('url', ['key' => 'value']); + } + + public function it_should_revoke_the_authentication_token( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse) + { + $mockedHttpClient->request( + 'post', + 'https://login.salesforce.com/services/oauth2/revoke', + [ + 'headers' => [ + 'content-type' => 'application/x-www-form-urlencoded' + ], + 'form_params' => [ + 'token' => $this->token + ] + ]) + ->shouldBeCalled() + ->willReturn($mockedResponse); + $this->revoke()->shouldReturn($mockedResponse); + } + + /* + * + * Specs below are for the parent class. + * + */ + + public function it_should_return_all_versions( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient->request('get', 'https://instance.salesforce.com/services/data', ['headers' => ['Authorization' => 'Oauth accessToken', 'Accept' => 'application/json', 'Content-Type' => 'application/json']])->shouldBeCalled()->willReturn($mockedResponse); + + $versionArray = json_decode($this->versionJSON, true); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($versionArray); + + $this->versions()->shouldReturn($versionArray); + } + + public function it_should_return_resources( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient->request('get', 'https://instance.salesforce.com/resources', ['headers' => ['Authorization' => 'Oauth accessToken', 'Accept' => 'application/json', 'Content-Type' => 'application/json']])->shouldBeCalled()->willReturn($mockedResponse); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->resources()->shouldReturn($this->decodedResponse); + } + + public function it_should_return_identity( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient + ->request('get', + 'https://login.salesforce.com/id/00Do0000000xxxxx/005o0000000xxxxx', + Argument::type('array')) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->identity()->shouldReturn($this->decodedResponse); + } + + public function it_should_return_limits( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient + ->request('get', + 'https://instance.salesforce.com/resources/limits', + Argument::type('array')) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->limits()->shouldReturn($this->decodedResponse); + } + + public function it_should_return_describe( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient + ->request('get', + 'https://instance.salesforce.com/resources/sobjects', + Argument::type('array')) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->describe()->shouldReturn($this->decodedResponse); + } + + public function it_should_return_query( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient + ->request('get', + 'https://instance.salesforce.com/services/data/v30.0/resource?q=query', + Argument::type('array')) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->query('query')->shouldReturn($this->decodedResponse); + } + + public function it_should_return_query_next( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient + ->request('get', + 'https://instance.salesforce.com/next', + Argument::type('array')) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->next('/next')->shouldReturn($this->decodedResponse); + } + + public function it_should_return_queryExplain( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient + ->request('get', + 'https://instance.salesforce.com/services/data/v30.0/resource?explain=queryExplain', + Argument::type('array')) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->queryExplain('queryExplain')->shouldReturn($this->decodedResponse); + } + + public function it_should_return_queryAll( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient + ->request('get', + 'https://instance.salesforce.com/services/data/v30.0/resource?q=queryAll', + Argument::type('array')) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->queryAll('queryAll')->shouldReturn($this->decodedResponse); + } + + public function it_should_return_quick_actions( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient + ->request('get', + 'https://instance.salesforce.com/services/data/v30.0/resource', + Argument::type('array')) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->quickActions()->shouldReturn($this->decodedResponse); + } + + public function it_should_return_search( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient + ->request('get', + 'https://instance.salesforce.com/services/data/v30.0/resource?q=search', + Argument::type('array')) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->search('search')->shouldReturn($this->decodedResponse); + } + + public function it_should_return_ScopeOrder( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient + ->request('get', + 'https://instance.salesforce.com/services/data/v30.0/resource/scopeOrder', + Argument::type('array')) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->scopeOrder()->shouldReturn($this->decodedResponse); + } + + public function it_should_return_search_layouts( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient + ->request('get', + 'https://instance.salesforce.com/services/data/v30.0/resource/layout/?q=objectList', + Argument::type('array')) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->searchLayouts('objectList')->shouldReturn($this->decodedResponse); + } + + public function it_should_return_suggested_articles( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient + ->request('get', + 'https://instance.salesforce.com/services/data/v30.0/resource/suggestTitleMatches?q=suggestedArticles', + Argument::type('array')) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->suggestedArticles('suggestedArticles')->shouldReturn($this->decodedResponse); + } + + public function it_should_return_suggested_queries( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient + ->request('get', + 'https://instance.salesforce.com/services/data/v30.0/resource/suggestSearchQueries?q=suggested', + Argument::type('array')) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->suggestedQueries('suggested')->shouldReturn($this->decodedResponse); + } + + public function it_should_return_custom_request( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient + ->request('get', + 'https://instance.salesforce.com/services/apexrest/FieldCase?foo=bar', + Argument::type('array')) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->custom('/FieldCase', ['parameters' => ['foo' => 'bar']]) + ->shouldReturn($this->decodedResponse); + } + + public function it_returns_a_json_resource( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient + ->request('get', + 'uri', + Argument::type('array')) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->request('uri', [])->shouldReturn($this->decodedResponse); + } + + public function it_returns_a_xml_resource( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse) + { + $mockedResponse->getBody()->shouldBeCalled()->willReturn($this->responseXML); + + $mockedHttpClient + ->request('get', + 'uri', + Argument::type('array')) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $this->request('uri', ['format' => 'xml'])->shouldReturnAnInstanceOf('SimpleXMLElement'); + } + + public function it_returns_an_unformatted_resource( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse) + { + $mockedResponse->getBody()->shouldBeCalled()->willReturn($this->responseNone); + + $mockedHttpClient + ->request('get', + 'uri', + Argument::type('array')) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $this->request('uri', ['format' => 'none'])->shouldReturn($this->responseNone); + } + + public function it_should_format_header( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient->request('get', + 'uri', + [ + 'headers' => [ + 'Authorization' => 'Oauth accessToken', + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ] + ]) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->request('uri', ['compression' => false])->shouldReturn($this->decodedResponse); + } + + public function it_should_format_header_in_url_encoding( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient->request('get', + 'uri', + [ + 'headers' => [ + 'Authorization' => 'bearer accesstoken', + 'Accept' => 'application/x-www-form-urlencoded', + 'Content-Type' => 'application/x-www-form-urlencoded', + ] + ]) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedFormatter->setHeaders()->shouldBeCalled()->willReturn([ + 'Authorization' => 'bearer accesstoken', + 'Accept' => 'application/x-www-form-urlencoded', + 'Content-Type' => 'application/x-www-form-urlencoded', + ]); + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn('rawresponse'); + + $this->request('uri', ['format' => 'urlencoded'])->shouldReturn('rawresponse'); + } + + public function it_should_format_header_with_gzip( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient->request('get', + 'uri', + [ + 'headers' => [ + 'Authorization' => 'bearer accesstoken', + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'Accept-Encoding' => 'gzip', + 'Content-Encoding' => 'gzip', + ] + ]) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + + $mockedFormatter->setHeaders()->shouldBeCalled()->willReturn([ + 'Authorization' => 'bearer accesstoken', + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'Accept-Encoding' => 'gzip', + 'Content-Encoding' => 'gzip', + ]); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->request('uri', ['compression' => true, 'compressionType' => 'gzip'])->shouldReturn($this->decodedResponse); + } + + public function it_should_format_header_with_deflate( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient->request('get', + 'uri', + [ + 'headers' => [ + 'Authorization' => 'bearer accesstoken', + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'Accept-Encoding' => 'deflate', + 'Content-Encoding' => 'deflate', + ] + ]) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedFormatter->setHeaders()->shouldBeCalled()->willReturn([ + 'Authorization' => 'bearer accesstoken', + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + 'Accept-Encoding' => 'deflate', + 'Content-Encoding' => 'deflate', + ]); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->request('uri', ['compression' => true, 'compressionType' => 'deflate'])->shouldReturn($this->decodedResponse); + } + + public function it_allows_access_to_the_guzzle_client(ClientInterface $mockedHttpClient) + { + $this->getClient()->shouldReturn($mockedHttpClient); + } + + public function it_should_allow_a_get_request( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient->request('GET', Argument::any(), Argument::any())->willReturn($mockedResponse); + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + $this->get('uri')->shouldReturn($this->decodedResponse); + } + + public function it_should_allow_a_post_request( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient->request('POST', Argument::any(), Argument::any())->willReturn($mockedResponse); + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + $this->post('uri', ['test' => 'param'])->shouldReturn($this->decodedResponse); + } + + public function it_should_allow_a_put_request( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient->request('PUT', Argument::any(), Argument::any())->willReturn($mockedResponse); + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + $this->put('uri', ['test' => 'param'])->shouldReturn($this->decodedResponse); + } + + public function it_should_allow_a_patch_request( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient->request('PATCH', Argument::any(), Argument::any())->willReturn($mockedResponse); + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + $this->patch('uri', ['test' => 'param'])->shouldReturn($this->decodedResponse); + } + + public function it_should_allow_a_head_request( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter + ) { + $mockedHttpClient->request('HEAD', Argument::any(), Argument::any())->willReturn($mockedResponse); + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->head('url')->shouldReturn($this->decodedResponse); + } + + public function it_should_allow_a_delete_request( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter + ) { + $mockedHttpClient->request('DELETE', Argument::any(), Argument::any())->willReturn($mockedResponse); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->delete('url')->shouldReturn($this->decodedResponse); + } + + public function it_should_fire_a_response_event( + ClientInterface $mockedHttpClient, + EventInterface $mockedEvent, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter + ) { + $mockedHttpClient->request(Argument::any(), Argument::any(), Argument::any())->willReturn($mockedResponse); + $mockedEvent->fire('forrest.response', Argument::any())->shouldBeCalled(); + + $this->versions(); + } + + public function it_should_not_encode_body_if_a_custom_header_is_used( + ClientInterface $mockedHttpClient, + ResponseInterface $mockedResponse, + FormatterInterface $mockedFormatter) + { + $mockedHttpClient->request('put', + 'https://instance.salesforce.com/services/data/v30.0/resource/ingest/123/batches', + [ + 'headers' => [ + 'Authorization' => 'Oauth accessToken', + 'Accept' => 'application/json', + 'Content-Type' => 'text/csv' + ], + 'body' => "id,name\r\n0010000000AAA,Rick Sanchez" + ]) + ->shouldBeCalled() + ->willReturn($mockedResponse); + + $mockedFormatter->formatResponse($mockedResponse)->shouldBeCalled()->willReturn($this->decodedResponse); + + $this->jobs('ingest/123/batches', [ + 'method' => 'put', + 'headers' => [ + 'Content-Type' => 'text/csv' + ], + 'body' => "id,name\r\n0010000000AAA,Rick Sanchez" + ])->shouldReturn($this->decodedResponse); + } +} diff --git a/src/Omniphx/Forrest/Authentications/ClientCredentials.php b/src/Omniphx/Forrest/Authentications/ClientCredentials.php new file mode 100644 index 0000000..db4a35a --- /dev/null +++ b/src/Omniphx/Forrest/Authentications/ClientCredentials.php @@ -0,0 +1,74 @@ +credentials['loginURL'] : $url; + $loginURL .= '/services/oauth2/token'; + + $authToken = $this->getAuthToken($loginURL); + + $this->tokenRepo->put($authToken); + + $this->storeVersion(); + $this->storeResources(); + } + + /** + * Refresh authentication token by re-authenticating. + * + * @return void + */ + public function refresh() + { + $tokenURL = $this->credentials['loginURL'] . '/services/oauth2/token'; + $authToken = $this->getAuthToken($tokenURL); + + $this->tokenRepo->put($authToken); + } + + /** + * @param String $tokenURL + * @param Array $parameters + * @return String + */ + private function getAuthToken($url) + { + $parameters['form_params'] = [ + 'grant_type' => 'client_credentials', + 'client_id' => $this->credentials['consumerKey'], + 'client_secret' => $this->credentials['consumerSecret'], + ]; + + // \Psr\Http\Message\ResponseInterface + $response = $this->httpClient->request('post', $url, $parameters); + + $authTokenDecoded = json_decode($response->getBody()->getContents(), true); + + $this->handleAuthenticationErrors($authTokenDecoded); + + return $authTokenDecoded; + } + + /** + * Revokes access token from Salesforce. Will not flush token from storage. + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function revoke() + { + $accessToken = $this->tokenRepo->get(); + $url = $this->credentials['loginURL'].'/services/oauth2/revoke'; + + $options['headers']['content-type'] = 'application/x-www-form-urlencoded'; + $options['form_params']['token'] = $accessToken; + + return $this->httpClient->request('post', $url, $options); + } +} diff --git a/src/Omniphx/Forrest/Client.php b/src/Omniphx/Forrest/Client.php index a943188..263d260 100644 --- a/src/Omniphx/Forrest/Client.php +++ b/src/Omniphx/Forrest/Client.php @@ -70,6 +70,8 @@ abstract class Client implements AuthenticationInterface */ protected $event; + protected $url; + protected $resourceRepo; protected $stateRepo; diff --git a/src/Omniphx/Forrest/Interfaces/ClientCredentialsInterface.php b/src/Omniphx/Forrest/Interfaces/ClientCredentialsInterface.php new file mode 100644 index 0000000..69e7406 --- /dev/null +++ b/src/Omniphx/Forrest/Interfaces/ClientCredentialsInterface.php @@ -0,0 +1,7 @@ + env('SF_AUTH_METHOD', 'WebServer'), @@ -24,7 +24,7 @@ // Only required for UserPassword authentication: 'username' => env('SF_USERNAME'), - // Security token might need to be ammended to password unless IP Address is whitelisted + // Security token might need to be amended to password unless IP Address is whitelisted 'password' => env('SF_PASSWORD'), // Only required for OAuthJWT authentication: 'privateKey' => '',