From a6c01534655c38ac3f06f0cd0a81aaf9941a40be Mon Sep 17 00:00:00 2001 From: Alejandro Ibarra Date: Mon, 11 Feb 2019 11:22:32 +0000 Subject: [PATCH] Initial commit --- .gitignore | 6 + .scrutinizer.yml | 35 ++++ .semver | 5 + .travis.yml | 27 +++ CONTRIBUTING.md | 4 + LICENSE | 26 +++ README.md | 130 ++++++++++++ composer.json | 39 ++++ phpunit.xml | 34 ++++ src/Provider/Cognito.php | 181 +++++++++++++++++ src/Provider/CognitoUser.php | 251 +++++++++++++++++++++++ test/src/Provider/CognitoTest.php | 320 ++++++++++++++++++++++++++++++ 12 files changed, 1058 insertions(+) create mode 100644 .gitignore create mode 100644 .scrutinizer.yml create mode 100644 .semver create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100755 LICENSE create mode 100644 composer.json create mode 100644 phpunit.xml create mode 100644 src/Provider/Cognito.php create mode 100644 src/Provider/CognitoUser.php create mode 100644 test/src/Provider/CognitoTest.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a11097 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/build +/vendor +composer.phar +composer.lock +.DS_Store +.idea diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..3454eb3 --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,35 @@ +filter: + excluded_paths: [test/*] +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, test] + php_cpd: + enabled: true + excluded_dirs: [vendor, test] \ No newline at end of file diff --git a/.semver b/.semver new file mode 100644 index 0000000..b6ebb03 --- /dev/null +++ b/.semver @@ -0,0 +1,5 @@ +--- +:major: 1 +:minor: 0 +:patch: 0 +:special: '' diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9e751a5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,27 @@ +language: php + +sudo: false + +php: + - 5.6 + - 7.0 + - 7.1 + - hhvm + +matrix: + include: + - php: 5.6 + env: 'COMPOSER_FLAGS="--prefer-stable --prefer-lowest"' + +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: + - wget https://scrutinizer-ci.com/ocular.phar + - php ocular.phar code-coverage:upload --format=php-clover coverage.clover \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..653b64f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,4 @@ +Contributing +============ + +This repository follows the [CakeDC Plugin Standard](https://www.cakedc.com/plugin-standard). If you'd like to contribute new features, enhancements or bug fixes to the plugin, please read our [Contribution Guidelines](https://www.cakedc.com/contribution-guidelines) for detailed instructions. diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..21f3349 --- /dev/null +++ b/LICENSE @@ -0,0 +1,26 @@ +The MIT License + +Copyright 2009-2019 +Cake Development Corporation +1785 E. Sahara Avenue, Suite 490-423 +Las Vegas, Nevada 89104 +Phone: +1 702 425 5085 +https://www.cakedc.com + +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 index e69de29..d9c3b1a 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,130 @@ +# Amazon Cognito Provider for OAuth 2.0 Client +[![Latest Version](https://img.shields.io/github/release/cakedc/oauth2-cognito.svg?style=flat-square)](https://github.com/cakedc/oauth2-cognito/releases) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) +[![Build Status](https://img.shields.io/travis/cakedc/oauth2-cognito/master.svg?style=flat-square)](https://travis-ci.org/cakedc/oauth2-cognito) +[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/cakedc/oauth2-cognito.svg?style=flat-square)](https://scrutinizer-ci.com/g/cakedc/oauth2-cognito/code-structure) +[![Quality Score](https://img.shields.io/scrutinizer/g/cakedc/oauth2-cognito.svg?style=flat-square)](https://scrutinizer-ci.com/g/cakedc/oauth2-cognito) +[![Total Downloads](https://img.shields.io/packagist/dt/cakedc/oauth2-cognito.svg?style=flat-square)](https://packagist.org/packages/cakedc/oauth2-cognito) + +This package provides Amazon Cognito 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 cakedc/oauth2-cognito +``` + +## Usage + +Usage is the same as The League's OAuth client, using `\CakeDC\OAuth2\Client\Provider\Cognito` as the provider. + +### Authorization Code Flow + +```php +$provider = new CakeDC\OAuth2\Client\Provider\Cognito([ + 'clientId' => '{cognito-client-id}', + 'clientSecret' => '{cognito-client-secret}', + 'redirectUri' => 'https://example.com/callback-url', + 'cognitoDomain' => '{cognito-client-domain}', +]); + +if (!isset($_GET['code'])) { + + // If we don't have an authorization code then get one + $authUrl = $provider->getAuthorizationUrl(); + $_SESSION['oauth2state'] = $provider->getState(); + header('Location: '.$authUrl); + exit; + +// Check given state against previously stored one to mitigate CSRF attack +} elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) { + + unset($_SESSION['oauth2state']); + exit('Invalid state'); + +} else { + + // Try to get an access token (using the authorization code grant) + $token = $provider->getAccessToken('authorization_code', [ + 'code' => $_GET['code'] + ]); + + // Optional: Now you have a token you can look up a users profile data + try { + + // We got an access token, let's now get the user's details + $user = $provider->getResourceOwner($token); + + // Use these details to create a new profile + printf('Hello %s!', $user->getName()); + + } catch (Exception $e) { + + // Failed to get user details + exit('Oh dear...'); + } + + // Use this to interact with an API on the users behalf + echo $token->getToken(); +} +``` + +### Managing Scopes + +When creating your Amazon Cognito authorization URL, you can specify the state and scopes your application may authorize. + +```php +$options = [ + 'state' => 'OPTIONAL_CUSTOM_CONFIGURED_STATE', + 'scope' => ['email','phone','profile'] +]; + +$authorizationUrl = $provider->getAuthorizationUrl($options); +``` +If neither are defined, the provider will utilize internal defaults. + +At the time of authoring this documentation, the [following scopes are available](https://instagram.com/developer/authentication/#scope). + +- phone +- email +- profile +- openid (required for phone, email or profile) +- + +##Hosted domain + +Optionally, if you are using your own domain for you client, you can configure it instead of `cognitoDomain` option when the provider is initialized: + +```php +$provider = new CakeDC\OAuth2\Client\Provider\Cognito([ + 'clientId' => '{cognito-client-id}', + 'clientSecret' => '{cognito-client-secret}', + 'redirectUri' => 'https://example.com/callback-url', + 'hostedDomain' => '{cognito-hosted-domain}', //Full domain without trailing slash +]); + +``` + +## Testing + +``` bash +$ ./vendor/bin/phpunit +``` + +## Contributing + +Please see [CONTRIBUTING](https://github.com/cakedc/oauth2-cognito/blob/master/CONTRIBUTING.md) for details. + + +## Credits + +- [Cake Development Corporation](https://github.com/cakedc) +- [Alejandro Ibarra](https://github.com/ajibarra) +- [All Contributors](https://github.com/cakedc/oauth2-cognito/contributors) + + +## License + +The MIT License (MIT). Please see [License File](https://github.com/cakedc/oauth2-cognito/blob/master/LICENSE) for more information. \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f2df90e --- /dev/null +++ b/composer.json @@ -0,0 +1,39 @@ +{ + "name": "cakedc/oauth2-cognito", + "description": "Cognito OAuth 2.0 Client Provider for The PHP League OAuth2-Client", + "license": "MIT", + "authors": [ + { + "name": "CakeDC", + "email": "team@cakedc.com", + "homepage": "https://www.cakedc.com" + } + ], + "keywords": [ + "oauth", + "oauth2", + "client", + "authorization", + "authentication", + "cognito" + ], + "require": { + "php": "^5.6 || ^7.0", + "league/oauth2-client": "^2.0" + }, + "require-dev": { + "phpunit/phpunit": "^8", + "mockery/mockery": "~0.9", + "squizlabs/php_codesniffer": "~2.0" + }, + "autoload": { + "psr-4": { + "CakeDC\\OAuth2\\Client\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "CakeDC\\OAuth2\\Client\\Test\\": "tests/src/" + } + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..9c9aaa6 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,34 @@ + + + + + + + + + ./test/ + + + + + ./ + + ./vendor + ./test + + + + \ No newline at end of file diff --git a/src/Provider/Cognito.php b/src/Provider/Cognito.php new file mode 100644 index 0000000..b9d173d --- /dev/null +++ b/src/Provider/Cognito.php @@ -0,0 +1,181 @@ +hostedDomain = $options['hostedDomain']; + } elseif (!empty($options['cognitoDomain'])) { + $this->cognitoDomain = $options['cognitoDomain']; + } else { + throw new \InvalidArgumentException( + 'Neither "cognitoDomain" nor "hostedDomain" options are set. Please set one of them.' + ); + } + + if (!empty($options['scope'])) { + $this->scopes = explode($this->getScopeSeparator(), $options['scope']); + } + } + + /** + * @return array + */ + public function getScopes() + { + return $this->scopes; + } + + /** + * @return string + */ + public function getHostedDomain() + { + return $this->hostedDomain; + } + + /** + * @param string $hostedDomain + */ + public function setHostedDomain($hostedDomain) + { + $this->hostedDomain = $hostedDomain; + } + + /** + * @return string + */ + public function getCognitoDomain() + { + return $this->cognitoDomain; + } + + /** + * @param string $cognitoDomain + */ + public function setCognitoDomain($cognitoDomain) + { + $this->cognitoDomain = $cognitoDomain; + } + + + /** + * Returns the url for given action + * + * @param $action + * @return string + */ + private function getCognitoUrl($action) + { + return !empty($this->hostedDomain) ? $this->hostedDomain . $action : + sprintf(self::BASE_COGNITO_URL, $this->cognitoDomain, $action); + } + + + public function getBaseAuthorizationUrl() + { + return $this->getCognitoUrl('/authorize'); + } + + public function getBaseAccessTokenUrl(array $params) + { + return $this->getCognitoUrl('/token'); + } + + public function getResourceOwnerDetailsUrl(AccessToken $token) + { + return $this->getCognitoUrl('/oauth2/userInfo'); + } + + protected function getAuthorizationParameters(array $options) + { + $scopes = array_merge($this->getDefaultScopes(), $this->scopes); + + if (!empty($options['scope'])) { + $scopes = array_merge($scopes, $options['scope']); + } + + $options['scope'] = array_unique($scopes); + + return parent::getAuthorizationParameters($options); + } + + protected function getDefaultScopes() + { + return ['openid', 'email']; + } + + protected function getScopeSeparator() + { + return ' '; + } + + protected function checkResponse(ResponseInterface $response, $data) + { + if (empty($data['error'])) { + return; + } + + $code = 0; + $error = $data['error']; + + throw new IdentityProviderException($error, $code, $data); + } + + protected function createResourceOwner(array $response, AccessToken $token) + { + $user = new CognitoUser($response); + + return $user; + } +} diff --git a/src/Provider/CognitoUser.php b/src/Provider/CognitoUser.php new file mode 100644 index 0000000..4530398 --- /dev/null +++ b/src/Provider/CognitoUser.php @@ -0,0 +1,251 @@ +data = $response; + } + + /** + * Get id + * + * @return string + */ + public function getId() + { + return $this->getField('sub'); + } + + /** + * Get address. + * + * @return string|null + */ + public function getAddress() + { + return $this->getField('address'); + } + + /** + * Get username. + * + * @return string|null + */ + public function getUsername() + { + return $this->getField('username'); + } + + /** + * Get email address. + * + * @return string|null + */ + public function getEmail() + { + return $this->getField('email'); + } + + /** + * Get email verified. + * + * @return string|null + */ + public function getEmailVerified() + { + return $this->getField('email_verified'); + } + + /** + * Get phone number. + * + * @return string|null + */ + public function getPhoneNumber() + { + return $this->getField('phone_number'); + } + + /** + * Get phone number verified. + * + * @return string|null + */ + public function getPhoneNumberVerified() + { + return $this->getField('phone_number_verified'); + } + + /** + * Get birthdate. + * + * @return string|null + */ + public function getBirthdate() + { + return $this->getField('birthdate'); + } + + /** + * Get profile. + * + * @return string|null + */ + public function getProfile() + { + return $this->getField('profile'); + } + + /** + * Get gender. + * + * @return string|null + */ + public function getGender() + { + return $this->getField('gender'); + } + + /** + * Get name. + * + * @return string|null + */ + public function getName() + { + return $this->getField('name'); + } + + /** + * Get given name. + * + * @return string|null + */ + public function getGivenName() + { + return $this->getField('given_name'); + } + + /** + * Get middle name. + * + * @return string|null + */ + public function getMiddleName() + { + return $this->getField('middle_name'); + } + + /** + * Get family name. + * + * @return string|null + */ + public function getFamilyName() + { + return $this->getField('family_name'); + } + + /** + * Get locale. + * + * @return string|null + */ + public function getLocale() + { + return $this->getField('locale'); + } + + /** + * Get zone info. + * + * @return string|null + */ + public function getZoneinfo() + { + return $this->getField('zoneinfo'); + } + + /** + * Get preferred username. + * + * @return string|null + */ + public function getPreferredUsername() + { + return $this->getField('preferred_username'); + } + + /** + * Get nickname. + * + * @return string|null + */ + public function getNickname() + { + return $this->getField('nickname'); + } + + /** + * Get website. + * + * @return string|null + */ + public function getWebsite() + { + return $this->getField('website'); + } + + /** + * Get picture. + * + * @return string|null + */ + public function getPicture() + { + return $this->getField('picture'); + } + + /** + * Get user data as an array. + * + * @return array + */ + public function toArray() + { + return $this->data; + } + + /** + * Returns a field from the Graph node data. + * + * @param string $key + * + * @return mixed|null + */ + private function getField($key) + { + return isset($this->data[$key]) ? $this->data[$key] : null; + } +} diff --git a/test/src/Provider/CognitoTest.php b/test/src/Provider/CognitoTest.php new file mode 100644 index 0000000..ed097fe --- /dev/null +++ b/test/src/Provider/CognitoTest.php @@ -0,0 +1,320 @@ +provider = new \CakeDC\OAuth2\Client\Provider\Cognito([ + 'clientId' => 'mock_client_id', + 'clientSecret' => 'mock_secret', + 'redirectUri' => 'none', + 'cognitoDomain' => 'mock_cognito_domain' + ]); + } + + public function tearDown() : void + { + Mockery::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 testNoHostedDomainNorCognitoDomain() + { + $this->expectException('\InvalidArgumentException'); + $this->expectExceptionMessage( + 'Neither "cognitoDomain" nor "hostedDomain" options are set. Please set one of them.' + ); + $provider = new \CakeDC\OAuth2\Client\Provider\Cognito([ + 'clientId' => 'mock_client_id', + 'clientSecret' => 'mock_secret', + 'redirectUri' => 'none', + ]); + } + + public function testSetScopeInConfig() + { + $provider = new \CakeDC\OAuth2\Client\Provider\Cognito([ + 'clientId' => 'mock_client_id', + 'clientSecret' => 'mock_secret', + 'redirectUri' => 'none', + 'hostedDomain' => 'mock_hosted_domain', + 'scope' => 'test-scope test-scope-2' + ]); + $this->assertEquals(['test-scope', 'test-scope-2'], $provider->getScopes()); + } + public function testSetHostedDomainInConfig() + { + $host = uniqid(); + + $provider = new \CakeDC\OAuth2\Client\Provider\Cognito([ + 'clientId' => 'mock_client_id', + 'clientSecret' => 'mock_secret', + 'redirectUri' => 'none', + 'hostedDomain' => $host + ]); + + $this->assertEquals($host, $provider->getHostedDomain()); + } + + public function testSetHostedDomainAfterConfig() + { + $host = uniqid(); + + $this->provider->setHostedDomain($host); + + $this->assertEquals($host, $this->provider->getHostedDomain()); + } + + public function testSetCognitoDomainAfterConfig() + { + $host = uniqid(); + + $this->provider->setCognitoDomain($host); + + $this->assertEquals($host, $this->provider->getCognitoDomain()); + } + + public function testSetCognitoDomainInConfig() + { + $host = uniqid(); + + $provider = new \CakeDC\OAuth2\Client\Provider\Cognito([ + 'clientId' => 'mock_client_id', + 'clientSecret' => 'mock_secret', + 'redirectUri' => 'none', + 'cognitoDomain' => $host + ]); + + $this->assertEquals($host, $provider->getCognitoDomain()); + } + + public function testScopes() + { + $scopeSeparator = ' '; + $options = ['scope' => [uniqid(), uniqid()]]; + $query = ['scope' => implode($scopeSeparator, $options['scope'])]; + $url = $this->provider->getAuthorizationUrl($options); + $query['scope'] = 'openid email ' . $query['scope']; + $encodedScope = $this->buildQueryString($query); + $this->assertStringContainsString($encodedScope, $url); + } + + public function testGetAuthorizationUrl() + { + $url = $this->provider->getAuthorizationUrl(); + $uri = parse_url($url); + + $this->assertEquals('/authorize', $uri['path']); + } + + public function testGetBaseAccessTokenUrl() + { + $params = []; + + $url = $this->provider->getBaseAccessTokenUrl($params); + $uri = parse_url($url); + + $this->assertEquals('/token', $uri['path']); + } + + public function testGetAccessToken() + { + $response = Mockery::mock('Psr\Http\Message\ResponseInterface'); + $response + ->shouldReceive('getBody') + ->andReturn( + '{"access_token":"mock_access_token","id_token": "mock_access_token", "token_type": "Bearer"}' + ); + $response->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); + + $client = Mockery::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->assertNull($token->getRefreshToken()); + $this->assertNull($token->getResourceOwnerId()); + } + + public function testUserData() + { + $userId = rand(1000, 9999); + $zoneinfo = uniqid(); + $website = uniqid(); + $address = uniqid(); + $birthdate = uniqid(); + $gender = uniqid(); + $profile = uniqid(); + $preferredUsername = uniqid(); + $locale = uniqid(); + $givenName = uniqid(); + $middleName = uniqid(); + $picture = uniqid(); + $name = uniqid(); + $nickname = uniqid(); + $phoneNumber = uniqid(); + $familyName = uniqid(); + $email = uniqid(); + $username = uniqid(); + + + $postResponse = Mockery::mock('Psr\Http\Message\ResponseInterface'); + $postResponse + ->shouldReceive('getBody') + ->andReturn( + '{"access_token":"mock_access_token","id_token": "mock_access_token", "token_type": "Bearer"}' + ); + $postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); + + $userResponse = Mockery::mock('Psr\Http\Message\ResponseInterface'); + $userResponse + ->shouldReceive('getBody') + ->andReturn( + '{"sub":"' . $userId . '","zoneinfo":"' . $zoneinfo . '","website":"' . $website . '", + "address":"' . $address . '","birthdate":"' . $birthdate . '","email_verified":"true", + "gender":"' . $gender . '","profile":"' . $profile . '","phone_number_verified":"true", + "preferred_username":"' . $preferredUsername . '","locale":"' . $locale . '", + "given_name":"' . $givenName . '","middle_name":"' . $middleName . '","picture":"' . $picture . '", + "name":"' . $name . '","nickname":"' . $nickname . '","phone_number":"' . $phoneNumber . '", + "family_name":"' . $familyName . '","email":"' . $email . '","username":"' . $username . '"}' + ); + $userResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); + + $client = Mockery::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']); + /** + * @var CognitoUser $user + */ + $user = $this->provider->getResourceOwner($token); + + $this->assertEquals($userId, $user->getId()); + $this->assertEquals($userId, $user->toArray()['sub']); + $this->assertEquals($name, $user->getName()); + $this->assertEquals($name, $user->toArray()['name']); + $this->assertEquals($zoneinfo, $user->getZoneinfo()); + $this->assertEquals($zoneinfo, $user->toArray()['zoneinfo']); + $this->assertEquals($website, $user->getWebsite()); + $this->assertEquals($website, $user->toArray()['website']); + $this->assertEquals($address, $user->getAddress()); + $this->assertEquals($address, $user->toArray()['address']); + $this->assertEquals($birthdate, $user->getBirthdate()); + $this->assertEquals($birthdate, $user->toArray()['birthdate']); + $this->assertEquals($gender, $user->getGender()); + $this->assertEquals($gender, $user->toArray()['gender']); + $this->assertEquals($profile, $user->getProfile()); + $this->assertEquals($profile, $user->toArray()['profile']); + $this->assertEquals($preferredUsername, $user->getPreferredUsername()); + $this->assertEquals($preferredUsername, $user->toArray()['preferred_username']); + $this->assertEquals($locale, $user->getLocale()); + $this->assertEquals($locale, $user->toArray()['locale']); + $this->assertEquals($givenName, $user->getGivenName()); + $this->assertEquals($givenName, $user->toArray()['given_name']); + $this->assertEquals($middleName, $user->getMiddleName()); + $this->assertEquals($middleName, $user->toArray()['middle_name']); + $this->assertEquals($picture, $user->getPicture()); + $this->assertEquals($picture, $user->toArray()['picture']); + $this->assertEquals($nickname, $user->getNickname()); + $this->assertEquals($nickname, $user->toArray()['nickname']); + $this->assertEquals($phoneNumber, $user->getPhoneNumber()); + $this->assertEquals($phoneNumber, $user->toArray()['phone_number']); + $this->assertEquals($familyName, $user->getFamilyName()); + $this->assertEquals($familyName, $user->toArray()['family_name']); + $this->assertEquals($email, $user->getEmail()); + $this->assertEquals($email, $user->toArray()['email']); + $this->assertEquals($username, $user->getUsername()); + $this->assertEquals($username, $user->toArray()['username']); + $this->assertEquals("true", $user->getPhoneNumberVerified()); + $this->assertEquals("true", $user->toArray()['phone_number_verified']); + $this->assertEquals("true", $user->getEmailVerified()); + $this->assertEquals("true", $user->toArray()['email_verified']); + } + + public function testExceptionThrownWhenError() + { + $message = uniqid(); + $this->expectException('League\OAuth2\Client\Provider\Exception\IdentityProviderException'); + $this->expectExceptionMessage($message); + $status = rand(400, 600); + $postResponse = Mockery::mock('Psr\Http\Message\ResponseInterface'); + $postResponse->shouldReceive('getBody')->andReturn('{"error":"'. $message .'"}'); + $postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); + $postResponse->shouldReceive('getReasonPhrase'); + $postResponse->shouldReceive('getStatusCode')->andReturn($status); + + $client = Mockery::mock('GuzzleHttp\ClientInterface'); + $client->shouldReceive('send') + ->times(1) + ->andReturn($postResponse); + $this->provider->setHttpClient($client); + $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']); + } + + public function testGetAuthenticatedRequest() + { + $method = 'GET'; + $url = 'https://test.auth.us-west-2.amazoncognito.com/oauth2/userInfo'; + + $accessTokenResponse = Mockery::mock('Psr\Http\Message\ResponseInterface'); + $accessTokenResponse + ->shouldReceive('getBody') + ->andReturn( + '{"access_token": "mock_access_token","user": {"sub": "1234","username": "test","name": "Test User"}}' + ); + $accessTokenResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); + + $client = Mockery::mock('GuzzleHttp\ClientInterface'); + $client->shouldReceive('send') + ->times(1) + ->andReturn($accessTokenResponse); + $this->provider->setHttpClient($client); + + $token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']); + + $authenticatedRequest = $this->provider->getAuthenticatedRequest($method, $url, $token); + + $this->assertInstanceOf('Psr\Http\Message\RequestInterface', $authenticatedRequest); + $this->assertEquals($method, $authenticatedRequest->getMethod()); + $this->assertStringContainsString( + 'Bearer mock_access_token', + $authenticatedRequest->getHeaders()['Authorization'][0] + ); + } +}