From 06c54cc285103c98eeec8fac0075963441bc567b Mon Sep 17 00:00:00 2001 From: Andrew Embler Date: Mon, 18 Feb 2019 18:49:13 +0000 Subject: [PATCH] Adding tests for class --- .gitignore | 3 +- LICENSE.TXT | 8 ++ README.md | 83 +++++++++++++++++ phpunit.xml | 18 ++++ src/Provider/Concrete5.php | 2 +- test/src/Provider/Concrete5Test.php | 134 ++++++++++++++++++++++++++++ 6 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 LICENSE.TXT create mode 100755 README.md create mode 100755 phpunit.xml create mode 100755 test/src/Provider/Concrete5Test.php diff --git a/.gitignore b/.gitignore index 4f337a6..012e960 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,9 @@ # Ignore composer stuff /vendor/ +composer.lock # Ignore developer stuff .idea node_modules -.env \ No newline at end of file +.env diff --git a/LICENSE.TXT b/LICENSE.TXT new file mode 100644 index 0000000..3e3f810 --- /dev/null +++ b/LICENSE.TXT @@ -0,0 +1,8 @@ +The MIT License +Copyright © 2008, Concrete CMS Inc. + +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 100755 index 0000000..ea93ec4 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# concrete5 provider for OAuth 2.0 Client + +This package provides concrete5 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 concrete5/oauth2-concrete5 +``` + +## Requirements + +You must be running concrete5 8.5.0 or greater on your concrete5 website. Additionally, you must create an API Integration with Standard User Consent from the Dashboard > System > API > Integrations page. This integration will create a client ID and a client secret that you will use below. + +## Usage + +Usage is the same as The League's OAuth client, using `\Concrete\OAuth2\Client\Provider\Concrete5` as the provider. + +### Authorization Code Flow + +```php +$provider = new Concrete\OAuth2\Client\Provider\Concrete5([ + 'clientId' => '{integrationClientId}', + 'clientSecret' => '{integrationClientSecret}', + 'redirectUri' => 'https://example.com/callback-url' +]); + +if (!isset($_GET['code'])) { + + $url = $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->getUserName()); + + } 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(); +} +``` + +## Testing + +``` bash +$ ./vendor/bin/phpunit +``` + +## Credits + +- [Andrew Embler](http://andrewembler.com) + + +## License + +The MIT License (MIT). Please see LICENSE.TXT for more info. \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100755 index 0000000..9447c87 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ + + + + + ./test/ + + + \ No newline at end of file diff --git a/src/Provider/Concrete5.php b/src/Provider/Concrete5.php index 10ce7cf..bd0f3db 100644 --- a/src/Provider/Concrete5.php +++ b/src/Provider/Concrete5.php @@ -85,7 +85,7 @@ protected function checkResponse(ResponseInterface $response, $data) { if ($response->getStatusCode() >= 400) { throw new IdentityProviderException( - $response->getReasonPhrase(), + $data['error'] ?: $response->getReasonPhrase(), $response->getStatusCode(), $response ); diff --git a/test/src/Provider/Concrete5Test.php b/test/src/Provider/Concrete5Test.php new file mode 100755 index 0000000..ea104fe --- /dev/null +++ b/test/src/Provider/Concrete5Test.php @@ -0,0 +1,134 @@ +provider = new Concrete5([ + 'clientId' => 'mock_client_id', + 'clientSecret' => 'mock_secret', + 'redirectUri' => 'http://none', + 'baseUrl' => 'http://example.com', + ]); + } + + 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 testGetBasenUrl() + { + $url = $this->provider->getBaseUrl(); + $this->assertEquals('http://example.com', $url); + } + + public function testGetAuthorizationUrl() + { + $url = $this->provider->getAuthorizationUrl(); + $uri = parse_url($url); + $this->assertEquals('/oauth/2.0/authorize', $uri['path']); + } + + public function testGetBaseAccessTokenUrl() + { + $params = []; + $url = $this->provider->getBaseAccessTokenUrl($params); + $uri = parse_url($url); + $this->assertEquals('/oauth/2.0/token', $uri['path']); + } + + public function testGetAccessToken() + { + $response = m::mock('Psr\Http\Message\ResponseInterface'); + $response->shouldReceive('getBody')->andReturn('{"access_token":"mock_access_token", "token_type":"bearer"}'); + $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->assertNull($token->getRefreshToken()); + $this->assertNull($token->getResourceOwnerId()); + } + + public function testUserData() + { + $response_data = [ + 'id' => rand(1000, 9999), + 'username' => uniqid(), + 'email' => uniqid(), + ]; + + $response_data_wrapped = ['data' => $response_data]; + + + $postResponse = m::mock('Psr\Http\Message\ResponseInterface'); + $postResponse->shouldReceive('getBody')->andReturn('{"access_token":"mock_access_token","expires_in":"3600","token_type":"Bearer","scope":"openid email profile","id_token":"mock_token_id"}'); + $postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); + $postResponse->shouldReceive('getStatusCode')->andReturn(200); + $userResponse = m::mock('Psr\Http\Message\ResponseInterface'); + $userResponse->shouldReceive('getBody')->andReturn(json_encode($response_data_wrapped)); + $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($response_data['id'], $user->getId()); + $this->assertEquals($response_data['id'], $user->toArray()['id']); + $this->assertEquals($response_data['email'], $user->getUserEmail()); + $this->assertEquals($response_data['email'], $user->toArray()['email']); + $this->assertEquals($response_data['username'], $user->getUserName()); + $this->assertEquals($response_data['username'], $user->toArray()['username']); + } + + /** + * @expectedException League\OAuth2\Client\Provider\Exception\IdentityProviderException + */ + public function testExceptionThrownWhenErrorObjectReceived() + { + $message = uniqid(); + $status = rand(400, 600); + $postResponse = m::mock('Psr\Http\Message\ResponseInterface'); + $postResponse->shouldReceive('getBody')->andReturn(' {"error":"'.$message.'"}'); + $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); + $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']); + } +} \ No newline at end of file