diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c9c8f2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/build +/vendor +composer.phar +composer.lock +.DS_Store diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..a903494 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,35 @@ +filter: + excluded_paths: [tests/*] +checks: + php: + code_rating: true + remove_extra_empty_lines: true + remove_php_closing_tag: true + remove_trailing_whitespace: true + fix_use_statements: + remove_unused: true + preserve_multiple: false + preserve_blanklines: true + order_alphabetically: true + fix_php_opening_tag: true + fix_linefeed: true + fix_line_ending: true + fix_identation_4spaces: true + fix_doc_comments: true +tools: + external_code_coverage: + timeout: 600 + runs: 2 + php_analyzer: true + php_code_coverage: false + php_code_sniffer: + config: + standard: PSR2 + filter: + paths: ['src'] + php_loc: + enabled: true + excluded_dirs: [vendor, tests] + php_cpd: + enabled: true + excluded_dirs: [vendor, tests] diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f20c368 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: php + +php: + - 5.5 + - 5.6 + - 7.0 + - hhvm + +before_script: + - travis_retry composer self-update + - travis_retry composer install --no-interaction --prefer-source --dev + - travis_retry phpenv rehash + +script: + - ./vendor/bin/phpcs --standard=psr2 src/ + - ./vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover + +after_script: + - if [ "$TRAVIS_PHP_VERSION" != "hhvm" ] && [ "$TRAVIS_PHP_VERSION" != "7.0" ]; then wget https://scrutinizer-ci.com/ocular.phar; fi + - if [ "$TRAVIS_PHP_VERSION" != "hhvm" ] && [ "$TRAVIS_PHP_VERSION" != "7.0" ]; then php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2315eff --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog +All Notable changes to `oauth2-salesforce` will be documented in this file + +## 0.1.0 - 2015-10-05 + +### Added +- Initial release! + +### Deprecated +- Nothing + +### Fixed +- Nothing + +### Removed +- Nothing + +### Security +- Nothing diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..9247aae --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,42 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +We accept contributions via Pull Requests on [Github](https://github.com/stevenmaguire/oauth2-salesforce). + + +## Pull Requests + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the README and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow SemVer. Randomly breaking public APIs is not an option. + +- **Create topic branches** - Don't ask us to pull from your master branch. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. + +- **Ensure tests pass!** - Please run the tests (see below) before submitting your pull request, and make sure they pass. We won't accept a patch until all tests pass. + +- **Ensure no coding standards violations** - Please run PHP Code Sniffer using the PSR-2 standard (see below) before submitting your pull request. A violation will cause the build to fail, so please make sure there are no violations. We can't accept a patch if the build fails. + + +## Running Tests + +``` bash +$ ./vendor/bin/phpunit +``` + + +## Running PHP Code Sniffer + +``` bash +$ ./vendor/bin/phpcs src --standard=psr2 -sp +``` + +**Happy coding**! diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..51455e2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Steven Maguire + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b99b2b --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# Salesforce Provider for OAuth 2.0 Client + +[![Latest Version](https://img.shields.io/github/release/stevenmaguire/oauth2-salesforce.svg?style=flat-square)](https://github.com/stevenmaguire/oauth2-salesforce/releases) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) +[![Build Status](https://img.shields.io/travis/stevenmaguire/oauth2-salesforce/master.svg?style=flat-square)](https://travis-ci.org/stevenmaguire/oauth2-salesforce) +[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/stevenmaguire/oauth2-salesforce.svg?style=flat-square)](https://scrutinizer-ci.com/g/stevenmaguire/oauth2-salesforce/code-structure) +[![Quality Score](https://img.shields.io/scrutinizer/g/stevenmaguire/oauth2-salesforce.svg?style=flat-square)](https://scrutinizer-ci.com/g/stevenmaguire/oauth2-salesforce) +[![Total Downloads](https://img.shields.io/packagist/dt/stevenmaguire/oauth2-salesforce.svg?style=flat-square)](https://packagist.org/packages/stevenmaguire/oauth2-salesforce) + +This package provides Salesforce OAuth 2.0 support for the PHP League's [OAuth 2.0 Client](https://github.com/thephpleague/oauth2-client). + +## Installation + +To install, use composer: + +``` +composer require stevenmaguire/oauth2-salesforce +``` + +## Usage + +Usage is the same as The League's OAuth client, using `\Stevenmaguire\OAuth2\Client\Provider\Salesforce` as the provider. + +### Authorization Code Flow + +```php +$provider = new Stevenmaguire\OAuth2\Client\Provider\Salesforce([ + 'clientId' => '{salesforce-client-id}', + 'clientSecret' => '{salesforce-client-secret}', + 'redirectUri' => 'https://example.com/callback-url' +]); +``` +For further usage of this package please refer to the [core package documentation on "Authorization Code Grant"](https://github.com/thephpleague/oauth2-client#usage). + +### Refreshing a Token + +```php +$provider = new Stevenmaguire\OAuth2\Client\Provider\Salesforce([ + 'clientId' => '{salesforce-client-id}', + 'clientSecret' => '{salesforce-client-secret}', + 'redirectUri' => 'https://example.com/callback-url' +]); + +$existingAccessToken = getAccessTokenFromYourDataStore(); + +if ($existingAccessToken->hasExpired()) { + $newAccessToken = $provider->getAccessToken('refresh_token', [ + 'refresh_token' => $existingAccessToken->getRefreshToken() + ]); + + // Purge old access token and store new access token to your data store. +} +``` + +For further usage of this package please refer to the [core package documentation on "Refreshing a Token"](https://github.com/thephpleague/oauth2-client#refreshing-a-token). + +## Testing + +``` bash +$ ./vendor/bin/phpunit +``` + +## Contributing + +Please see [CONTRIBUTING](https://github.com/stevenmaguire/oauth2-salesforce/blob/master/CONTRIBUTING.md) for details. + + +## Credits + +- [Steven Maguire](https://github.com/stevenmaguire) +- [All Contributors](https://github.com/stevenmaguire/oauth2-salesforce/contributors) + + +## License + +The MIT License (MIT). Please see [License File](https://github.com/stevenmaguire/oauth2-salesforce/blob/master/LICENSE) for more information. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b1d609e --- /dev/null +++ b/composer.json @@ -0,0 +1,44 @@ +{ + "name": "stevenmaguire/oauth2-salesforce", + "description": "Salesforce OAuth 2.0 Client Provider for The PHP League OAuth2-Client", + "license": "MIT", + "authors": [ + { + "name": "Steven Maguire", + "email": "stevenmaguire@gmail.com", + "homepage": "https://github.com/stevenmaguire" + } + ], + "keywords": [ + "oauth", + "oauth2", + "client", + "authorization", + "authorisation", + "salesforce" + ], + "require": { + "php": ">=5.5.0", + "league/oauth2-client": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0", + "mockery/mockery": "~0.9", + "squizlabs/php_codesniffer": "~2.0" + }, + "autoload": { + "psr-4": { + "Stevenmaguire\\OAuth2\\Client\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Stevenmaguire\\OAuth2\\Client\\Test\\": "tests/src/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..b486c71 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,37 @@ + + + + + + + + + ./tests/ + + + + + ./ + + ./vendor + ./tests + + + + diff --git a/src/Provider/Salesforce.php b/src/Provider/Salesforce.php new file mode 100644 index 0000000..d0f4e84 --- /dev/null +++ b/src/Provider/Salesforce.php @@ -0,0 +1,105 @@ +getResourceOwnerId(); + } + + /** + * Get the default scopes used by this provider. + * + * This should not be a complete list of all scopes, but the minimum + * required for the provider user interface! + * + * @return array + */ + protected function getDefaultScopes() + { + return []; + } + + /** + * Returns the string that should be used to separate scopes when building + * the URL for requesting an access token. + * + * @return string Scope separator, defaults to ',' + */ + protected function getScopeSeparator() + { + return ' '; + } + + /** + * Check a provider response for errors. + * + * @throws IdentityProviderException + * @param ResponseInterface $response + * @param string $data Parsed response data + * @return void + */ + protected function checkResponse(ResponseInterface $response, $data) + { + $statusCode = $response->getStatusCode(); + if ($statusCode >= 400) { + throw new IdentityProviderException( + isset($data[0]['message']) ? $data[0]['message'] : $response->getReasonPhrase(), + $statusCode, + $response + ); + } + } + + /** + * Generate a user object from a successful user details request. + * + * @param object $response + * @param AccessToken $token + * @return League\OAuth2\Client\Provider\ResourceOwnerInterface + */ + protected function createResourceOwner(array $response, AccessToken $token) + { + return new SalesforceResourceOwner($response); + } +} diff --git a/src/Provider/SalesforceResourceOwner.php b/src/Provider/SalesforceResourceOwner.php new file mode 100644 index 0000000..d2180b5 --- /dev/null +++ b/src/Provider/SalesforceResourceOwner.php @@ -0,0 +1,110 @@ +response = $response; + } + + /** + * Get user id + * + * @return string|null + */ + public function getId() + { + return $this->getResponseData('user_id'); + } + + /** + * Get user first name + * + * @return string|null + */ + public function getFirstName() + { + return $this->getResponseData('first_name'); + } + + /** + * Get user last name + * + * @return string|null + */ + public function getLastName() + { + return $this->getResponseData('last_name'); + } + + /** + * Get user email + * + * @return string|null + */ + public function getEmail() + { + return $this->getResponseData('email'); + } + + /** + * Get user title + * + * @return string|null + */ + public function getTitle() + { + return $this->getResponseData('custom_attributes.title'); + } + + /** + * Attempts to pull value from array using dot notation. + * + * @param string $path + * @param string $default + * + * @return mixed + */ + protected function getResponseData($path, $default = null) + { + $array = $this->response; + + if (!empty($path)) { + $keys = explode('.', $path); + + foreach ($keys as $key) { + if (isset($array[$key])) { + $array = $array[$key]; + } else { + return $default; + } + } + } + + return $array; + } + + /** + * Return all of the owner details available as an array. + * + * @return array + */ + public function toArray() + { + return $this->response; + } +} diff --git a/tests/access_token_response.json b/tests/access_token_response.json new file mode 100644 index 0000000..2a4deed --- /dev/null +++ b/tests/access_token_response.json @@ -0,0 +1,11 @@ +{ + "id": "mock_id", + "issued_at": "1296458209517", + "scope": "mock_scope", + "instance_url": "mock_instance", + "token_type": "Bearer", + "refresh_token": "mock_refresh_token", + "id_token": "mock_id_token", + "signature": "mock_signature", + "access_token": "mock_access_token" +} diff --git a/tests/src/Provider/SalesforceTest.php b/tests/src/Provider/SalesforceTest.php new file mode 100644 index 0000000..be0e406 --- /dev/null +++ b/tests/src/Provider/SalesforceTest.php @@ -0,0 +1,151 @@ +provider = new \Stevenmaguire\OAuth2\Client\Provider\Salesforce([ + 'clientId' => 'mock_client_id', + 'clientSecret' => 'mock_secret', + 'redirectUri' => 'none', + ]); + } + + protected function getJsonFile($file, $encode = false) + { + $json = file_get_contents(dirname(dirname(dirname(__FILE__))).'/'.$file); + $data = json_decode($json, true); + + if ($encode && json_last_error() == JSON_ERROR_NONE) { + return $data; + } + + return $json; + } + + public function tearDown() + { + m::close(); + parent::tearDown(); + } + + public function testAuthorizationUrl() + { + $url = $this->provider->getAuthorizationUrl(); + $uri = parse_url($url); + parse_str($uri['query'], $query); + + $this->assertArrayHasKey('client_id', $query); + $this->assertArrayHasKey('redirect_uri', $query); + $this->assertArrayHasKey('state', $query); + $this->assertArrayHasKey('scope', $query); + $this->assertArrayHasKey('response_type', $query); + $this->assertArrayHasKey('approval_prompt', $query); + $this->assertNotNull($this->provider->getState()); + } + + + public function testScopes() + { + $options = ['scope' => [uniqid(),uniqid()]]; + + $url = $this->provider->getAuthorizationUrl($options); + + $this->assertContains(urlencode(implode(' ', $options['scope'])), $url); + } + + public function testGetAuthorizationUrl() + { + $url = $this->provider->getAuthorizationUrl(); + $uri = parse_url($url); + + $this->assertEquals('login.salesforce.com', $uri['host']); + $this->assertEquals('/services/oauth2/authorize', $uri['path']); + } + + public function testGetBaseAccessTokenUrl() + { + $params = []; + + $url = $this->provider->getBaseAccessTokenUrl($params); + $uri = parse_url($url); + + $this->assertEquals('login.salesforce.com', $uri['host']); + $this->assertEquals('/services/oauth2/token', $uri['path']); + } + + public function testGetAccessToken() + { + $accessToken = $this->getJsonFile('access_token_response.json'); + $response = m::mock('Psr\Http\Message\ResponseInterface'); + $response->shouldReceive('getBody')->andReturn($accessToken); + $response->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); + $response->shouldReceive('getStatusCode')->andReturn(200); + + $client = m::mock('GuzzleHttp\ClientInterface'); + $client->shouldReceive('send')->times(1)->andReturn($response); + $this->provider->setHttpClient($client); + + $token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']); + + $this->assertEquals('mock_access_token', $token->getToken()); + $this->assertNull($token->getExpires()); + $this->assertEquals('mock_refresh_token', $token->getRefreshToken()); + $this->assertEquals('mock_id', $token->getResourceOwnerId()); + } + + public function testUserData() + { + $accessToken = $this->getJsonFile('access_token_response.json'); + $userJson = $this->getJsonFile('user_response.json'); + $userData = json_decode($userJson, true); + + $postResponse = m::mock('Psr\Http\Message\ResponseInterface'); + $postResponse->shouldReceive('getBody')->andReturn($accessToken); + $postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); + $postResponse->shouldReceive('getStatusCode')->andReturn(200); + + $userResponse = m::mock('Psr\Http\Message\ResponseInterface'); + $userResponse->shouldReceive('getBody')->andReturn($userJson); + $userResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); + $userResponse->shouldReceive('getStatusCode')->andReturn(200); + + $client = m::mock('GuzzleHttp\ClientInterface'); + $client->shouldReceive('send') + ->times(2) + ->andReturn($postResponse, $userResponse); + $this->provider->setHttpClient($client); + + $token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']); + $user = $this->provider->getResourceOwner($token); + + $this->assertEquals($userData, $user->toArray()); + $this->assertEquals($userData['user_id'], $user->getId()); + $this->assertEquals($userData['first_name'], $user->getFirstName()); + $this->assertEquals($userData['last_name'], $user->getLastName()); + $this->assertEquals($userData['email'], $user->getEmail()); + $this->assertNull($user->getTitle()); + } + + /** + * @expectedException League\OAuth2\Client\Provider\Exception\IdentityProviderException + **/ + public function testExceptionThrownWhenErrorObjectReceived() + { + $status = rand(401,599); + $postResponse = m::mock('Psr\Http\Message\ResponseInterface'); + $postResponse->shouldReceive('getBody')->andReturn('[{"errorCode": "INVALID_SESSION_ID","message": "Session expired or invalid"}]'); + $postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); + $postResponse->shouldReceive('getStatusCode')->andReturn($status); + $client = m::mock('GuzzleHttp\ClientInterface'); + $client->shouldReceive('send') + ->times(1) + ->andReturn($postResponse); + $this->provider->setHttpClient($client); + $token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']); + } +} diff --git a/tests/user_response.json b/tests/user_response.json new file mode 100644 index 0000000..1c34c79 --- /dev/null +++ b/tests/user_response.json @@ -0,0 +1,48 @@ +{ + "id":"https://login.salesforce.com/id/00D50000000IZ3ZEAW/00550000001fg5OAAQ", + "asserted_user":true, + "user_id":"00550000001fg5OAAQ", + "organization_id":"00D50000000IZ3ZEAW", + "username":"user@ example. com", + "nick_name":"user1.2950476911907334E12", + "display_name":"Sample User", + "email":"user@example.com", + "email_verified": true, + "first_name": "Sample", + "last_name": "User", + "status":{ + "created_date":"2010-11-08T20:55:33.000+0000", + "body":"Working on OAuth 2.0 article" + }, + "photos":{ + "picture":"https://c.na1.content.force.com/profilephoto/005/F", + "thumbnail":"https://c.na1.content.force.com/profilephoto/005/T" + }, + "addr_street": null, + "addr_city": null, + "addr_state": "ca", + "addr_country": "US", + "addr_zip": null, + "mobile_phone": null, + "mobile_phone_verified": false, + "urls":{ + "enterprise":"https://na1.salesforce.com/services/Soap/c/{version}/00D50000000IZ3Z", + "metadata":"https://na1.salesforce.com/services/Soap/m/{version}/00D50000000IZ3Z", + "partner":"https://na1.salesforce.com/services/Soap/u/{version}/00D50000000IZ3Z", + "rest":"https://na1.salesforce.com/services/data/v{version}/", + "sobjects":"https://na1.salesforce.com/services/data/v{version}/sobjects/", + "search":"https://na1.salesforce.com/services/data/v{version}/search/", + "query":"https://na1.salesforce.com/services/data/v{version}/query/", + "recent":"https://na1.salesforce.com/services/data/v{version}/recent/", + "profile":"https://na1.salesforce.com/00550000001fg5OAAQ" + }, + "active":true, + "user_type":"STANDARD", + "language":"en_US", + "locale":"en_US", + "utcOffset":-28800000, + "last_modified_date":"2011-01-14T23:28:01.000+0000", + "is_app_installed": true, + "custom_attributes": { + } +}