├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src └── Provider │ ├── Bitbucket.php │ └── BitbucketResourceOwner.php └── test └── src └── Provider └── BitbucketTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /vendor 3 | composer.phar 4 | composer.lock 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: [test/*] 3 | checks: 4 | php: 5 | code_rating: true 6 | remove_extra_empty_lines: true 7 | remove_php_closing_tag: true 8 | remove_trailing_whitespace: true 9 | fix_use_statements: 10 | remove_unused: true 11 | preserve_multiple: false 12 | preserve_blanklines: true 13 | order_alphabetically: true 14 | fix_php_opening_tag: true 15 | fix_linefeed: true 16 | fix_line_ending: true 17 | fix_identation_4spaces: true 18 | fix_doc_comments: true 19 | tools: 20 | external_code_coverage: 21 | timeout: 600 22 | runs: 3 23 | php_analyzer: true 24 | php_code_coverage: false 25 | php_code_sniffer: 26 | config: 27 | standard: PSR2 28 | filter: 29 | paths: ['src'] 30 | php_loc: 31 | enabled: true 32 | excluded_dirs: [vendor, test] 33 | php_cpd: 34 | enabled: true 35 | excluded_dirs: [vendor, test] 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | 5 | php: 6 | - 5.6 7 | - 7.0 8 | - 7.1 9 | - hhvm 10 | 11 | matrix: 12 | include: 13 | - php: 5.6 14 | env: 'COMPOSER_FLAGS="--prefer-stable --prefer-lowest"' 15 | 16 | before_script: 17 | - travis_retry composer self-update 18 | - travis_retry composer install --no-interaction --prefer-source --dev 19 | - travis_retry phpenv rehash 20 | 21 | script: 22 | - ./vendor/bin/phpcs --standard=psr2 src/ 23 | - ./vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover 24 | 25 | after_script: 26 | - wget https://scrutinizer-ci.com/ocular.phar 27 | - php ocular.phar code-coverage:upload --format=php-clover coverage.clover 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All Notable changes to `oauth2-bitbucket` will be documented in this file 3 | 4 | ## 3.0.0 - 2018-05-03 5 | 6 | ### Added 7 | - Updated scope separator from comma to space - thanks @GVSO (https://github.com/stevenmaguire/oauth2-bitbucket/pull/7) 8 | 9 | ### Deprecated 10 | - Nothing 11 | 12 | ### Fixed 13 | - Nothing 14 | 15 | ### Removed 16 | - Nothing 17 | 18 | ### Security 19 | - Nothing 20 | 21 | ## 2.0.0 - 2017-01-25 22 | 23 | ### Added 24 | - PHP 7.1 Support 25 | 26 | ### Deprecated 27 | - Nothing 28 | 29 | ### Fixed 30 | - Nothing 31 | 32 | ### Removed 33 | - PHP 5.5 Support 34 | 35 | ### Security 36 | - Nothing 37 | 38 | ## 1.0.0 - 2017-01-25 39 | 40 | Bump for base package parity 41 | 42 | ## 0.1.2 - 2016-02-06 43 | 44 | ### Added 45 | - Improved support for error response handling. 46 | 47 | ### Deprecated 48 | - Nothing 49 | 50 | ### Fixed 51 | - Nothing 52 | 53 | ### Removed 54 | - Nothing 55 | 56 | ### Security 57 | - Nothing 58 | 59 | ## 0.1.1 - 2015-08-20 60 | 61 | ### Added 62 | - Changelog 63 | 64 | ### Deprecated 65 | - Nothing 66 | 67 | ### Fixed 68 | - Nothing 69 | 70 | ### Removed 71 | - Nothing 72 | 73 | ### Security 74 | - Nothing 75 | 76 | ## 0.1.0 - 2015-08-20 77 | 78 | ### Added 79 | - Initial release! 80 | 81 | ### Deprecated 82 | - Nothing 83 | 84 | ### Fixed 85 | - Nothing 86 | 87 | ### Removed 88 | - Nothing 89 | 90 | ### Security 91 | - Nothing 92 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/stevenmaguire/oauth2-bitbucket). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[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). 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the README and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow SemVer. Randomly breaking public APIs is not an option. 17 | 18 | - **Create topic branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **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. 23 | 24 | - **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. 25 | 26 | - **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. 27 | 28 | 29 | ## Running Tests 30 | 31 | ``` bash 32 | $ ./vendor/bin/phpunit 33 | ``` 34 | 35 | 36 | ## Running PHP Code Sniffer 37 | 38 | ``` bash 39 | $ ./vendor/bin/phpcs src --standard=psr2 -sp 40 | ``` 41 | 42 | **Happy coding**! 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Steven Maguire 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bitbucket Provider for OAuth 2.0 Client 2 | 3 | [![Latest Version](https://img.shields.io/github/release/stevenmaguire/oauth2-bitbucket.svg?style=flat-square)](https://github.com/stevenmaguire/oauth2-bitbucket/releases) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 5 | [![Build Status](https://img.shields.io/travis/stevenmaguire/oauth2-bitbucket/master.svg?style=flat-square)](https://travis-ci.org/stevenmaguire/oauth2-bitbucket) 6 | [![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/stevenmaguire/oauth2-bitbucket.svg?style=flat-square)](https://scrutinizer-ci.com/g/stevenmaguire/oauth2-bitbucket/code-structure) 7 | [![Quality Score](https://img.shields.io/scrutinizer/g/stevenmaguire/oauth2-bitbucket.svg?style=flat-square)](https://scrutinizer-ci.com/g/stevenmaguire/oauth2-bitbucket) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/stevenmaguire/oauth2-bitbucket.svg?style=flat-square)](https://packagist.org/packages/stevenmaguire/oauth2-bitbucket) 9 | 10 | This package provides Bitbucket OAuth 2.0 support for the PHP League's [OAuth 2.0 Client](https://github.com/thephpleague/oauth2-client). 11 | 12 | ## Installation 13 | 14 | To install, use composer: 15 | 16 | ``` 17 | composer require stevenmaguire/oauth2-bitbucket 18 | ``` 19 | 20 | ## Usage 21 | 22 | Usage is the same as The League's OAuth client, using `\Stevenmaguire\OAuth2\Client\Provider\Bitbucket` as the provider. 23 | 24 | ### Authorization Code Flow 25 | 26 | ```php 27 | $provider = new Stevenmaguire\OAuth2\Client\Provider\Bitbucket([ 28 | 'clientId' => '{bitbucket-client-id}', 29 | 'clientSecret' => '{bitbucket-client-secret}', 30 | 'redirectUri' => 'https://example.com/callback-url' 31 | ]); 32 | 33 | if (!isset($_GET['code'])) { 34 | 35 | // If we don't have an authorization code then get one 36 | $authUrl = $provider->getAuthorizationUrl(); 37 | $_SESSION['oauth2state'] = $provider->getState(); 38 | header('Location: '.$authUrl); 39 | exit; 40 | 41 | // Check given state against previously stored one to mitigate CSRF attack 42 | } elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) { 43 | 44 | unset($_SESSION['oauth2state']); 45 | exit('Invalid state'); 46 | 47 | } else { 48 | 49 | // Try to get an access token (using the authorization code grant) 50 | $token = $provider->getAccessToken('authorization_code', [ 51 | 'code' => $_GET['code'] 52 | ]); 53 | 54 | // Optional: Now you have a token you can look up a users profile data 55 | try { 56 | 57 | // We got an access token, let's now get the user's details 58 | $user = $provider->getResourceOwner($token); 59 | 60 | // Use these details to create a new profile 61 | printf('Hello %s!', $user->getId()); 62 | 63 | } catch (Exception $e) { 64 | 65 | // Failed to get user details 66 | exit('Oh dear...'); 67 | } 68 | 69 | // Use this to interact with an API on the users behalf 70 | echo $token->getToken(); 71 | } 72 | ``` 73 | 74 | ## Testing 75 | 76 | ``` bash 77 | $ ./vendor/bin/phpunit 78 | ``` 79 | 80 | ## Contributing 81 | 82 | Please see [CONTRIBUTING](https://github.com/stevenmaguire/oauth2-bitbucket/blob/master/CONTRIBUTING.md) for details. 83 | 84 | 85 | ## Credits 86 | 87 | - [Steven Maguire](https://github.com/stevenmaguire) 88 | - [All Contributors](https://github.com/stevenmaguire/oauth2-bitbucket/contributors) 89 | 90 | 91 | ## License 92 | 93 | The MIT License (MIT). Please see [License File](https://github.com/stevenmaguire/oauth2-bitbucket/blob/master/LICENSE) for more information. 94 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stevenmaguire/oauth2-bitbucket", 3 | "description": "Bitbucket OAuth 2.0 Client Provider for The PHP League OAuth2-Client", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Steven Maguire", 8 | "email": "stevenmaguire@gmail.com", 9 | "homepage": "https://github.com/stevenmaguire" 10 | } 11 | ], 12 | "keywords": [ 13 | "oauth", 14 | "oauth2", 15 | "client", 16 | "authorization", 17 | "authorisation", 18 | "bitbucket" 19 | ], 20 | "require": { 21 | "league/oauth2-client": "^2.0" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "~4.0", 25 | "mockery/mockery": "~0.9", 26 | "squizlabs/php_codesniffer": "~2.0" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "Stevenmaguire\\OAuth2\\Client\\": "src/" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "Stevenmaguire\\OAuth2\\Client\\Test\\": "tests/src/" 36 | } 37 | }, 38 | "extra": { 39 | "branch-alias": { 40 | "dev-master": "1.0.x-dev" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 20 | 22 | 23 | 24 | 25 | ./test/ 26 | 27 | 28 | 29 | 30 | ./ 31 | 32 | ./vendor 33 | ./test 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/Provider/Bitbucket.php: -------------------------------------------------------------------------------- 1 | getValueByKey($data, $error)) { 88 | throw new IdentityProviderException($message, $response->getStatusCode(), $response); 89 | } 90 | }, $errors); 91 | } 92 | 93 | /** 94 | * Generate a user object from a successful user details request. 95 | * 96 | * @param object $response 97 | * @param AccessToken $token 98 | * @return League\OAuth2\Client\Provider\ResourceOwnerInterface 99 | */ 100 | protected function createResourceOwner(array $response, AccessToken $token) 101 | { 102 | return new BitbucketResourceOwner($response); 103 | } 104 | 105 | /** 106 | * Returns a prepared request for requesting an access token. 107 | * 108 | * @param array $params Query string parameters 109 | * @return Psr\Http\Message\RequestInterface 110 | */ 111 | protected function getAccessTokenRequest(array $params) 112 | { 113 | $request = parent::getAccessTokenRequest($params); 114 | $uri = $request->getUri() 115 | ->withUserInfo($this->clientId, $this->clientSecret); 116 | 117 | return $request->withUri($uri); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Provider/BitbucketResourceOwner.php: -------------------------------------------------------------------------------- 1 | response = $response; 22 | } 23 | 24 | /** 25 | * Get resource owner id 26 | * 27 | * @return string|null 28 | */ 29 | public function getId() 30 | { 31 | return $this->response['uuid'] ?: null; 32 | } 33 | 34 | /** 35 | * Get resource owner name 36 | * 37 | * @return string|null 38 | */ 39 | public function getName() 40 | { 41 | return $this->response['display_name'] ?: null; 42 | } 43 | 44 | /** 45 | * Get resource owner username 46 | * 47 | * @return string|null 48 | */ 49 | public function getUsername() 50 | { 51 | return $this->response['username'] ?: null; 52 | } 53 | 54 | /** 55 | * Get resource owner location 56 | * 57 | * @return string|null 58 | */ 59 | public function getLocation() 60 | { 61 | return $this->response['location'] ?: null; 62 | } 63 | 64 | /** 65 | * Return all of the owner details available as an array. 66 | * 67 | * @return array 68 | */ 69 | public function toArray() 70 | { 71 | return $this->response; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/src/Provider/BitbucketTest.php: -------------------------------------------------------------------------------- 1 | provider = new \Stevenmaguire\OAuth2\Client\Provider\Bitbucket([ 15 | 'clientId' => 'mock_client_id', 16 | 'clientSecret' => 'mock_client_secret', 17 | 'redirectUri' => 'redirect_url', 18 | ]); 19 | } 20 | 21 | public function tearDown() 22 | { 23 | m::close(); 24 | parent::tearDown(); 25 | } 26 | 27 | public function testAuthorizationUrl() 28 | { 29 | $url = $this->provider->getAuthorizationUrl(); 30 | $uri = parse_url($url); 31 | parse_str($uri['query'], $query); 32 | 33 | $this->assertArrayHasKey('client_id', $query); 34 | $this->assertArrayHasKey('redirect_uri', $query); 35 | $this->assertArrayHasKey('state', $query); 36 | $this->assertArrayHasKey('scope', $query); 37 | $this->assertArrayHasKey('response_type', $query); 38 | $this->assertArrayHasKey('approval_prompt', $query); 39 | $this->assertNotNull($this->provider->getState()); 40 | } 41 | 42 | 43 | public function testScopes() 44 | { 45 | $scopeSeparator = ' '; 46 | $options = ['scope' => [uniqid(), uniqid()]]; 47 | $query = ['scope' => implode($scopeSeparator, $options['scope'])]; 48 | $url = $this->provider->getAuthorizationUrl($options); 49 | $encodedScope = $this->buildQueryString($query); 50 | $this->assertContains($encodedScope, $url); 51 | } 52 | 53 | public function testGetAuthorizationUrl() 54 | { 55 | $url = $this->provider->getAuthorizationUrl(); 56 | $uri = parse_url($url); 57 | 58 | $this->assertEquals('/site/oauth2/authorize', $uri['path']); 59 | } 60 | 61 | public function testGetBaseAccessTokenUrl() 62 | { 63 | $params = []; 64 | 65 | $url = $this->provider->getBaseAccessTokenUrl($params); 66 | $uri = parse_url($url); 67 | 68 | $this->assertEquals('/site/oauth2/access_token', $uri['path']); 69 | } 70 | 71 | public function testGetAccessToken() 72 | { 73 | $response = m::mock('Psr\Http\Message\ResponseInterface'); 74 | $response->shouldReceive('getBody')->andReturn('{"access_token": "mock_access_token","scopes": "account","expires_in": 3600,"refresh_token": "mock_refresh_token","token_type": "bearer"}'); 75 | $response->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); 76 | 77 | $client = m::mock('GuzzleHttp\ClientInterface'); 78 | $client->shouldReceive('send')->times(1)->andReturn($response); 79 | $this->provider->setHttpClient($client); 80 | 81 | $token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']); 82 | 83 | $this->assertEquals('mock_access_token', $token->getToken()); 84 | $this->assertLessThanOrEqual(time() + 3600, $token->getExpires()); 85 | $this->assertGreaterThanOrEqual(time(), $token->getExpires()); 86 | $this->assertEquals('mock_refresh_token', $token->getRefreshToken()); 87 | $this->assertNull($token->getResourceOwnerId()); 88 | } 89 | 90 | public function testUserData() 91 | { 92 | $userId = rand(1000,9999); 93 | $name = uniqid(); 94 | $username = uniqid(); 95 | $location = uniqid(); 96 | 97 | $postResponse = m::mock('Psr\Http\Message\ResponseInterface'); 98 | $postResponse->shouldReceive('getBody')->andReturn('{"access_token": "mock_access_token","scopes": "account","expires_in": 3600,"refresh_token": "mock_refresh_token","token_type": "bearer"}'); 99 | $postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); 100 | 101 | $userResponse = m::mock('Psr\Http\Message\ResponseInterface'); 102 | $userResponse->shouldReceive('getBody')->andReturn('{"created_on": "2011-12-20T16:34:07.132459+00:00","display_name": "'.$name.'","location": "'.$location.'","type": "user","username": "'.$username.'","uuid": "'.$userId.'","website": "https://tutorials.bitbucket.org/"}'); 103 | $userResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); 104 | 105 | $client = m::mock('GuzzleHttp\ClientInterface'); 106 | $client->shouldReceive('send') 107 | ->times(2) 108 | ->andReturn($postResponse, $userResponse); 109 | $this->provider->setHttpClient($client); 110 | 111 | $token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']); 112 | $user = $this->provider->getResourceOwner($token); 113 | 114 | $this->assertEquals($userId, $user->getId()); 115 | $this->assertEquals($userId, $user->toArray()['uuid']); 116 | $this->assertEquals($name, $user->getName()); 117 | $this->assertEquals($name, $user->toArray()['display_name']); 118 | $this->assertEquals($username, $user->getUsername()); 119 | $this->assertEquals($username, $user->toArray()['username']); 120 | $this->assertEquals($location, $user->getLocation()); 121 | $this->assertEquals($location, $user->toArray()['location']); 122 | } 123 | 124 | public function testUserDataFails() 125 | { 126 | $errorPayloads = [ 127 | '{"error":"mock_error","error_description": "mock_error_description"}', 128 | '{"error":{"message":"mock_error"},"error_description": "mock_error_description"}', 129 | '{"foo":"bar"}' 130 | ]; 131 | 132 | $testPayload = function ($payload) { 133 | $postResponse = m::mock('Psr\Http\Message\ResponseInterface'); 134 | $postResponse->shouldReceive('getBody')->andReturn('{"access_token": "mock_access_token","scopes": "account","expires_in": 3600,"refresh_token": "mock_refresh_token","token_type": "bearer"}'); 135 | $postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); 136 | 137 | $userResponse = m::mock('Psr\Http\Message\ResponseInterface'); 138 | $userResponse->shouldReceive('getBody')->andReturn($payload); 139 | $userResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']); 140 | $userResponse->shouldReceive('getStatusCode')->andReturn(500); 141 | 142 | $client = m::mock('GuzzleHttp\ClientInterface'); 143 | $client->shouldReceive('send') 144 | ->times(2) 145 | ->andReturn($postResponse, $userResponse); 146 | $this->provider->setHttpClient($client); 147 | 148 | $token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']); 149 | 150 | try { 151 | $user = $this->provider->getResourceOwner($token); 152 | return false; 153 | } catch (\Exception $e) { 154 | $this->assertInstanceOf('\League\OAuth2\Client\Provider\Exception\IdentityProviderException', $e); 155 | } 156 | 157 | return $payload; 158 | }; 159 | 160 | $this->assertCount(2, array_filter(array_map($testPayload, $errorPayloads))); 161 | } 162 | } 163 | --------------------------------------------------------------------------------