├── .gitmodules ├── tests ├── bootstrap.php └── OAuth2 │ └── Tests │ ├── Strategy │ ├── BaseTest.php │ ├── PasswordTest.php │ └── AuthCodeTest.php │ ├── TestCase.php │ ├── ResponseTest.php │ ├── AccessTokenTest.php │ └── ClientTest.php ├── .travis.yml ├── .gitignore ├── src └── OAuth2 │ ├── Strategy │ ├── Base.php │ ├── Password.php │ └── AuthCode.php │ ├── Error.php │ ├── Response.php │ ├── Client.php │ └── AccessToken.php ├── phpunit.xml.dist ├── composer.json └── README.md /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | setExpectedException('\InvalidArgumentException'); 14 | new \OAuth2\Strategy\Base; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/OAuth2/Strategy/Base.php: -------------------------------------------------------------------------------- 1 | client = $client; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | tests/OAuth2/ 17 | 18 | 19 | 20 | 21 | 22 | src/OAuth2/ 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/OAuth2/Tests/TestCase.php: -------------------------------------------------------------------------------- 1 | getMock('\OAuth2\Client', ['getResponse'], [$client_id, $client_secret, $opts]); 19 | 20 | // configure client stub 21 | $client->expects($this->any()) 22 | ->method('getResponse') 23 | ->will($this->returnCallback([$this, 'mockGetResponse'])); 24 | 25 | return $client; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/OAuth2/Error.php: -------------------------------------------------------------------------------- 1 | error = $this; 19 | $this->response = $response; 20 | 21 | $parsedResponse = $response->parse(); 22 | if (is_array($parsedResponse)) { 23 | $this->code = isset($parsedResponse['error']) ? $parsedResponse['error'] : 0; 24 | $this->message = isset($parsedResponse['error_description']) ? $parsedResponse['error_description'] : null; 25 | } 26 | } 27 | 28 | /** 29 | * response getter 30 | * 31 | * @return OAuth2\Response 32 | */ 33 | public function getResponse() 34 | { 35 | return $this->response; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/OAuth2/Strategy/Password.php: -------------------------------------------------------------------------------- 1 | 'password' 27 | , 'username' => $username 28 | , 'password' => $password 29 | ), $params); 30 | return $this->client->getToken($params, $opts); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keeguon/oauth2-php" 3 | , "type": "library" 4 | , "description": "A library to consume services using the OAuth 2 security scheme." 5 | , "keywords": ["oauth2"] 6 | , "homepage": "https://github.com/Keeguon/oauth2-php" 7 | , "version": "1.3.6" 8 | , "license": "MIT" 9 | , "authors": [ 10 | { 11 | "name": "Félix Bellanger" 12 | , "email": "felix.bellanger@gmail.com" 13 | , "homepage": "https://github.com/Keeguon" 14 | }, 15 | { 16 | "name": "Martin Jantošovič" 17 | , "homepage": "https://github.com/Mordred" 18 | }, 19 | { 20 | "name": "Cyrus David" 21 | , "email": "david@jcyr.us" 22 | , "homepage": "https://github.com/vohof" 23 | } 24 | ] 25 | , "require": { 26 | "php": ">=5.4" 27 | , "guzzlehttp/guzzle": "~5.0" 28 | } 29 | , "autoload": { 30 | "psr-0": { 31 | "OAuth2\\Tests": "tests/" 32 | , "OAuth2": "src/" 33 | } 34 | } 35 | , "require-dev": { 36 | "symfony/class-loader": "*" 37 | , "phpunit/phpunit": "*" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/OAuth2/Strategy/AuthCode.php: -------------------------------------------------------------------------------- 1 | 'code' 17 | , 'client_id' => $this->client->getId() 18 | ), $params); 19 | } 20 | 21 | /** 22 | * The authorization URL endpoint of the provider 23 | * 24 | * @param array $params Additional query parameters for the URL 25 | * @return string 26 | */ 27 | public function authorizeUrl($params = array()) 28 | { 29 | $params = array_merge($this->authorizeParams(), $params); 30 | return $this->client->authorizeUrl($params); 31 | } 32 | 33 | /** 34 | * Retrieve an access token given the specified validation code. 35 | * 36 | * @param string $code The Authorization Code value 37 | * @param array $params Additional params 38 | * @param array $opts Options 39 | */ 40 | public function getToken($code, $params = array(), $opts = array()) 41 | { 42 | $params = array_merge(array( 43 | 'grant_type' => 'authorization_code' 44 | , 'code' => $code 45 | ), $params); 46 | return $this->client->getToken($params, $opts); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/OAuth2/Response.php: -------------------------------------------------------------------------------- 1 | response = $response; 15 | $this->parseMode = $parseMode; 16 | } 17 | 18 | /** 19 | * response getter 20 | * 21 | * @return Guzzle\Http\Message\Response 22 | */ 23 | public function getResponse() 24 | { 25 | return $this->response; 26 | } 27 | 28 | public function headers() 29 | { 30 | return $this->response->getHeaders(); 31 | } 32 | 33 | public function status() 34 | { 35 | return $this->response->getStatusCode(); 36 | } 37 | 38 | public function body() 39 | { 40 | return (string) $this->response->getBody(); 41 | } 42 | 43 | public function parse() 44 | { 45 | $parsed = null; 46 | 47 | switch ($this->parseMode) { 48 | case 'json': 49 | $parsed = json_decode($this->body(), true); 50 | break; 51 | 52 | case 'query': 53 | parse_str($this->body(), $parsed); 54 | break; 55 | 56 | case 'automatic': 57 | default: 58 | $types = array('application/json', 'text/javascript'); 59 | $content_type = $this->content_type(); 60 | 61 | foreach ($types as $type) { 62 | if (stripos($content_type, $type) !== false) { 63 | $parsed = json_decode($this->body(), true); 64 | break; 65 | } 66 | } 67 | 68 | if (stripos($content_type, "application/x-www-form-urlencoded") !== false) { 69 | parse_str($this->body(), $parsed); 70 | } 71 | break; 72 | } 73 | 74 | return $parsed; 75 | } 76 | 77 | public function content_type() 78 | { 79 | return $this->response->getHeader('Content-Type'); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OAuth2-PHP [![Build Status](https://secure.travis-ci.org/Keeguon/oauth2-php.png)](http://travis-ci.org/Keeguon/oauth2-php) 2 | 3 | 4 | A PHP library aimed to consume services using OAuth 2 as a security scheme. 5 | 6 | 7 | ## Dependencies 8 | 9 | * PHP >=5.3.2 10 | * Guzzle 11 | 12 | 13 | ## Installation 14 | 15 | ### composer 16 | 17 | To install OAuth2-PHP with composer you simply need to create a composer.json in your project root and add: 18 | 19 | ```json 20 | { 21 | "require": { 22 | "keeguon/oauth2-php": ">=1.0.0" 23 | } 24 | } 25 | ``` 26 | 27 | Then run 28 | 29 | ```bash 30 | $ wget -nc http://getcomposer.org/composer.phar 31 | $ php composer.phar install 32 | ``` 33 | 34 | You have now OAuth2-PHP installed in vendor/keeguon/oauth2-php 35 | 36 | And an handy autoload file to include in you project in vendor/.composer/autoload.php 37 | 38 | 39 | ## Testing 40 | 41 | The library is fully tested with PHPUnit for unit tests. To run tests you need PHPUnit which can be installed using the project dependencies as follows: 42 | 43 | ```bash 44 | $ php composer.phar install --dev 45 | ``` 46 | 47 | Then to run the test suites 48 | 49 | ```bash 50 | $ vendor/bin/phpunit 51 | ``` 52 | 53 | 54 | ## License 55 | 56 | Copyright (c) 2012 Félix Bellanger 57 | 58 | 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: 59 | 60 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 61 | 62 | 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. 63 | -------------------------------------------------------------------------------- /tests/OAuth2/Tests/Strategy/PasswordTest.php: -------------------------------------------------------------------------------- 1 | client = $this->getClientStub('abc', 'def', array('site' => 'https://api.example.com')); 19 | 20 | // create password 21 | $this->password = new \OAuth2\Strategy\Password($this->client); 22 | } 23 | 24 | protected function tearDown() 25 | { 26 | unset($this->access); 27 | unset($this->client); 28 | unset($this->mode); 29 | unset($this->password); 30 | } 31 | 32 | /** 33 | * @covers OAuth2\Strategy\Password::authorize_url() 34 | */ 35 | public function testAuthorizeUrl() 36 | { 37 | $this->setExpectedException('\ErrorException', 'The authorization endpoint is not used in this strategy.'); 38 | $this->password->authorizeUrl(); 39 | } 40 | 41 | /** 42 | * @covers OAuth2\Strategy\Password::get_token() 43 | */ 44 | public function testGetToken() 45 | { 46 | foreach(array('json', 'formencoded') as $mode) { 47 | // get_token (mode) 48 | $this->mode = $mode; 49 | $this->access = $this->password->getToken('username', 'password'); 50 | 51 | // returns AccessToken with same Client 52 | $this->assertEquals($this->client, $this->access->getClient()); 53 | 54 | // returns AccessToken with $token 55 | $this->assertEquals('salmon', $this->access->getToken()); 56 | 57 | // returns AccessToken with $refresh_token 58 | $this->assertEquals('trout', $this->access->getRefreshToken()); 59 | 60 | // returns AccessToken with $expires_in 61 | $this->assertEquals(600, $this->access->getExpiresIn()); 62 | } 63 | } 64 | 65 | /** 66 | * Intercept all OAuth2\Client::getResponse() calls and mock their responses 67 | */ 68 | public function mockGetResponse() 69 | { 70 | // retrieve args 71 | $args = func_get_args(); 72 | 73 | // map responses 74 | $map = array( 75 | 'formencoded' => new \GuzzleHttp\Message\Response(200, array('Content-Type' => 'application/x-www-form-urlencoded'), \GuzzleHttp\Stream\Stream::factory('expires_in=600&access_token=salmon&refresh_token=trout')) 76 | , 'json' => new \GuzzleHttp\Message\Response(200, array('Content-Type' => 'application/json'), \GuzzleHttp\Stream\Stream::factory('{"expires_in":600,"access_token":"salmon","refresh_token":"trout"}')) 77 | ); 78 | 79 | return new \OAuth2\Response($map[$this->mode]); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/OAuth2/Tests/ResponseTest.php: -------------------------------------------------------------------------------- 1 | array('bar')); 22 | $body = 'foo'; 23 | 24 | // returns the status, headers and body 25 | $this->response = new \OAuth2\Response(new \GuzzleHttp\Message\Response($status, $headers, \GuzzleHttp\Stream\Stream::factory($body))); 26 | $responseHeaders = $this->response->headers(); 27 | $this->assertCount(1, $responseHeaders); 28 | $this->assertArrayHasKey('foo', $responseHeaders); 29 | $this->assertEquals($headers['foo'], $responseHeaders['foo']); 30 | $this->assertEquals($status, $this->response->status()); 31 | $this->assertEquals($body, $this->response->body()); 32 | } 33 | 34 | /** 35 | * @covers OAuth2\Response::content_type() 36 | * @covers OAuth2\Response::parse() 37 | */ 38 | public function testParseResponse() 39 | { 40 | // parses application/x-www-form-urlencoded body 41 | $headers = array('Content-Type' => 'application/x-www-form-urlencoded'); 42 | $body = 'foo=bar&answer=42'; 43 | $this->response = new \OAuth2\Response(new \GuzzleHttp\Message\Response(200, $headers, \GuzzleHttp\Stream\Stream::factory($body))); 44 | $parsedResponse = $this->response->parse(); 45 | $this->assertEquals(2, count(array_keys($parsedResponse))); 46 | $this->assertEquals('bar', $parsedResponse['foo']); 47 | $this->assertEquals(42, $parsedResponse['answer']); 48 | 49 | // parses application/json body 50 | $headers = array('Content-Type' => 'application/json'); 51 | $body = json_encode(array('foo' => 'bar', 'answer' => 42)); 52 | $this->response = new \OAuth2\Response(new \GuzzleHttp\Message\Response(200, $headers, \GuzzleHttp\Stream\Stream::factory($body))); 53 | $parsedResponse = $this->response->parse(); 54 | $this->assertEquals(2, count(array_keys($parsedResponse))); 55 | $this->assertEquals('bar', $parsedResponse['foo']); 56 | $this->assertEquals(42, $parsedResponse['answer']); 57 | 58 | // doesn't try to parse other content-types 59 | $headers = array('Content-Type' => 'text/html'); 60 | $body = ''; 61 | $this->response = new \OAuth2\Response(new \GuzzleHttp\Message\Response(200, $headers, \GuzzleHttp\Stream\Stream::factory($body))); 62 | $this->assertNull($this->response->parse()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/OAuth2/Tests/Strategy/AuthCodeTest.php: -------------------------------------------------------------------------------- 1 | kvformToken = 'expires_in=600&access_token=salmon&refresh_token=trout&extra_param=steve'; 22 | $this->jsonToken = json_encode(array('expires_in' => 600, 'access_token' => 'salmon', 'refresh_token' => 'trout', 'extra_param' => 'steve')); 23 | 24 | // get client stub 25 | $this->client = $this->getClientStub('abc', 'def', array('site' => 'https://api.example.com')); 26 | 27 | // create authCode 28 | $this->authCode = new \OAuth2\Strategy\AuthCode($this->client); 29 | } 30 | 31 | protected function tearDown() 32 | { 33 | unset($this->authCode); 34 | unset($this->access); 35 | unset($this->client); 36 | unset($this->code); 37 | unset($this->jsonToken); 38 | unset($this->kvformToken); 39 | unset($this->mode); 40 | } 41 | 42 | /** 43 | * @covers OAuth2\Strategy\AuthCode::authorize_url() 44 | */ 45 | public function testAuthorizeUrl() 46 | { 47 | // should include the client_id 48 | $this->assertContains('client_id=abc', $this->authCode->authorizeUrl()); 49 | 50 | // should include the type 51 | $this->assertContains('response_type=code', $this->authCode->authorizeUrl()); 52 | 53 | // should include passed in options 54 | $cb = 'http://myserver.local/oauth/callback'; 55 | $this->assertContains('redirect_uri='.urlencode($cb), $this->authCode->authorizeUrl(array('redirect_uri' => $cb))); 56 | } 57 | 58 | /** 59 | * @covers OAuth2\Strategy\AuthCode::get_token() 60 | */ 61 | public function testGetToken() 62 | { 63 | foreach(array('json', 'formencoded') as $mode) { 64 | // set mode 65 | $this->mode = $mode; 66 | 67 | foreach (array('GET', 'POST') as $verb) { 68 | // set token_method and get token 69 | $this->client->options['token_method'] = $verb; 70 | $this->access = $this->authCode->getToken($this->code); 71 | } 72 | 73 | // returns AccessToken with same Client 74 | $this->assertEquals($this->client, $this->access->getClient()); 75 | 76 | // returns AccessToken with $token 77 | $this->assertEquals('salmon', $this->access->getToken()); 78 | 79 | // returns AccessToken with $refresh_token 80 | $this->assertEquals('trout', $this->access->getRefreshToken()); 81 | 82 | // returns AccessToken with $expires_in 83 | $this->assertEquals(600, $this->access->getExpiresIn()); 84 | 85 | // returns AccessToken with params accessible via the params array 86 | $this->assertEquals('steve', $this->access->getParam('extra_param')); 87 | } 88 | } 89 | 90 | /** 91 | * Intercept all OAuth2\Client::getResponse() calls and mock their responses 92 | */ 93 | public function mockGetResponse() 94 | { 95 | // retrieve args 96 | $args = func_get_args(); 97 | 98 | // map responses 99 | $map = array( 100 | 'formencoded' => new \GuzzleHttp\Message\Response(200, array('Content-Type' => 'application/x-www-form-urlencoded'), \GuzzleHttp\Stream\Stream::factory($this->kvformToken)) 101 | , 'json' => new \GuzzleHttp\Message\Response(200, array('Content-Type' => 'application/json'), \GuzzleHttp\Stream\Stream::factory($this->jsonToken)) 102 | ); 103 | 104 | return new \OAuth2\Response($map[$this->mode]); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/OAuth2/Client.php: -------------------------------------------------------------------------------- 1 | id = $clientId; 17 | $this->secret = $clientSecret; 18 | if (isset($opts['site'])) { 19 | $this->site = ['base_url' => $opts['site']]; 20 | unset($opts['site']); 21 | } 22 | 23 | // Default options 24 | $this->options = array_merge([ 25 | 'authorize_url' => '/oauth/authorize' 26 | , 'token_url' => '/oauth/token' 27 | , 'token_method' => 'POST' 28 | , 'client_auth' => 'header' 29 | , 'request_opts' => [ 'exceptions' => true ] 30 | ], $opts); 31 | 32 | // Connection object using Guzzle 33 | $this->connection = new \GuzzleHttp\Client($this->site); 34 | } 35 | 36 | /** 37 | * id getter 38 | * 39 | * @return string 40 | */ 41 | public function getId() 42 | { 43 | return $this->id; 44 | } 45 | 46 | /** 47 | * secret getter 48 | * 49 | * @return string 50 | */ 51 | public function getSecret() 52 | { 53 | return $this->secret; 54 | } 55 | 56 | /** 57 | * The authorize endpoint URL of the OAuth2 provider 58 | * 59 | * @param array $params Additional query parameters 60 | * @return string 61 | */ 62 | public function authorizeUrl($params = array()) 63 | { 64 | $authorizeUrl = (strpos($this->options['authorize_url'], 'http') === 0) ? $this->options['authorize_url'] : $this->site['base_url'].$this->options['authorize_url']; 65 | return (count($params)) ? $authorizeUrl.'?'.http_build_query($params) : $authorizeUrl; 66 | } 67 | 68 | /** 69 | * The token endpoint URL of the OAuth2 provider 70 | * 71 | * @param array $params Additional query parameters 72 | * @return string 73 | */ 74 | public function tokenUrl($params = array()) 75 | { 76 | $tokenUrl = (strpos($this->options['token_url'], 'http') === 0) ? $this->options['token_url'] : $this->site['base_url'].$this->options['token_url']; 77 | return (count($params)) ? $tokenUrl.'?'.http_build_query($params) : $tokenUrl; 78 | } 79 | 80 | /** 81 | * Makes a request relative to the specified site root. 82 | * 83 | * @param string $verb One of the following http method: GET, POST, PUT, DELETE 84 | * @param string $url URL path of the request 85 | * @param array $opts The options to make the request with (possible options: params (array), body (string), headers (array), exceptions (boolean), parse ('automatic', 'query' or 'json') 86 | * @return \GuzzleHttp\Message\Request 87 | */ 88 | public function createRequest($verb, $url, $opts = array()) 89 | { 90 | // Set some default options 91 | $opts = array_merge(array( 92 | 'body' => '' 93 | , 'query' => array() 94 | , 'headers' => array() 95 | ), $this->options['request_opts'], $opts); 96 | 97 | // Create the request 98 | $verb = (in_array($verb, ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'OPTIONS', 'PATCH']) ? $verb : 'GET'); 99 | $request = $this->connection->createRequest($verb, $url, $opts); 100 | 101 | return $request; 102 | } 103 | 104 | /** 105 | * Initializes an AccessToken by making a request to the token endpoint 106 | * 107 | * @param \GuzzleHttp\Message\Request $request The request object 108 | * @param string $parseMode The mode of parsing for the response 109 | * @return \OAuth2\Response 110 | */ 111 | public function getResponse($request, $parseMode = 'automatic') 112 | { 113 | return new \OAuth2\Response($this->connection->send($request), $parseMode); 114 | } 115 | 116 | /** 117 | * Initializes an AccessToken by making a request to the token endpoint 118 | * 119 | * @param array $params An array of params for the token endpoint 120 | * @param array $access Token options, to pass to the AccessToken object 121 | * @return \OAuth2\AccessToken 122 | */ 123 | public function getToken($params = array(), $tokenOpts = array()) 124 | { 125 | // Get parse mode for the response 126 | $parseMode = isset($params['parse']) ? $params['parse'] : 'automatic'; 127 | unset($params['parse']); 128 | 129 | if ($this->options['token_method'] === 'POST') { 130 | $opts['headers'] = array('Content-Type' => 'x-www-form-urlencoded'); 131 | $opts['body'] = $params; 132 | } else { 133 | $opts['query'] = $params; 134 | } 135 | 136 | // Create request 137 | $request = $this->createRequest($this->options['token_method'], $this->tokenUrl(), $opts); 138 | 139 | // Set auth 140 | if (isset($this->options['client_auth'])) { 141 | if ($this->options['client_auth'] === 'header') { 142 | $request->setHeader('Authorization', 'Basic ' . base64_encode("$this->id:$this->secret")); 143 | } else if ($this->options['client_auth'] === 'query') { 144 | $request->getQuery()->merge(['client_id' => $this->id, 'client_secret' => $this->secret]); 145 | } else if ($this->options['client_auth'] === 'body') { 146 | // Retrieve current body as a \Guzzle\Query object since we'll have to add client auth 147 | $body = \GuzzleHttp\Query::fromString((string) $request->getBody()); 148 | 149 | // Add client auth 150 | $body->merge(['client_id' => $this->id, 'client_secret' => $this->secret]); 151 | 152 | // Replace body 153 | $request->setBody(\GuzzleHttp\Stream\Stream::factory((string) $body)); 154 | } else { 155 | throw new \Exception("Unknown client authentication method."); 156 | } 157 | } else { 158 | throw new \Exception("Missing client authentication method."); 159 | } 160 | 161 | // Get response 162 | $response = $this->getResponse($request, $parseMode); 163 | 164 | // Handle response 165 | $parsedResponse = $response->parse(); 166 | if (!is_array($parsedResponse) && !isset($parsedResponse['access_token'])) { 167 | throw new \OAuth2\Error($response); 168 | } 169 | 170 | // Return access token 171 | return \OAuth2\AccessToken::fromHash($this, array_merge($parsedResponse, $tokenOpts)); 172 | } 173 | 174 | /** 175 | * The Authorization Code strategy 176 | * 177 | * @return \OAuth2\Strategy\AuthCode 178 | */ 179 | public function authCode() 180 | { 181 | $this->authCode = isset($this->authCode) ? $this->authCode : new \OAuth2\Strategy\AuthCode($this); 182 | return $this->authCode; 183 | } 184 | 185 | /** 186 | * The Resource Owner Password Credentials strategy 187 | * 188 | * @return \OAuth2\Strategy\Password 189 | */ 190 | public function password() 191 | { 192 | $this->password = isset($this->password) ? $this->password : new \OAuth2\Strategy\Password($this); 193 | return $this->password; 194 | } 195 | } 196 | 197 | -------------------------------------------------------------------------------- /tests/OAuth2/Tests/AccessTokenTest.php: -------------------------------------------------------------------------------- 1 | tokenBody = json_encode(array('access_token' => 'foo', 'expires_in' => 600, 'refresh_token' => 'bar')); 28 | $this->refreshBody = json_encode(array('access_token' => 'refreshed_foo', 'expires_in' => 600, 'refresh_token' => 'refresh_bar')); 29 | 30 | // get client stub 31 | $this->client = $this->getClientStub('abc', 'def', array('site' => 'https://api.example.com')); 32 | 33 | // instantiate access_token 34 | $this->accessToken = new \OAuth2\AccessToken($this->client, $this->token); 35 | } 36 | 37 | protected function tearDown() 38 | { 39 | unset($this->accessToken); 40 | unset($this->client); 41 | unset($this->refreshBody); 42 | unset($this->token); 43 | unset($this->tokenBody); 44 | } 45 | 46 | /** 47 | * @covers OAuth2\AccessToken::__construct() 48 | */ 49 | public function testConstructorBuildsAccessToken() 50 | { 51 | // assigns client and token 52 | $this->assertEquals($this->client, $this->accessToken->getClient()); 53 | $this->assertEquals($this->token, $this->accessToken->getToken()); 54 | 55 | // assigns extra params 56 | $target = new \OAuth2\AccessToken($this->client, $this->token, array('foo' => 'bar')); 57 | $this->assertArrayHasKey('foo', $target->getParams()); 58 | $this->assertEquals('bar', $target->getParam('foo')); 59 | 60 | // initialize with a Hash 61 | $hash = array('access_token' => $this->token, 'expires_in' => time() + 200, 'foo' => 'bar'); 62 | $target = \OAuth2\AccessToken::fromHash($this->client, $hash); 63 | $this->assertInitializeToken($target); 64 | 65 | // initalizes with a form-urlencoded key/value string 66 | $kvform = "access_token={$this->token}&expires_in={time() + 200}&foo=bar"; 67 | $target = \OAuth2\AccessToken::fromKvform($this->client, $kvform); 68 | $this->assertInitializeToken($target); 69 | 70 | // sets options 71 | $target = new \OAuth2\AccessToken($this->client, $this->token, array('param_name' => 'foo', 'header_format' => 'Bearer %', 'mode' => 'body')); 72 | $this->assertEquals('foo', $target->options['param_name']); 73 | $this->assertEquals('Bearer %', $target->options['header_format']); 74 | $this->assertEquals('body', $target->options['mode']); 75 | } 76 | 77 | /** 78 | * @covers OAuth2\AccessToken::request() 79 | * @covers OAuth2\AccessToken::get() 80 | * @covers OAuth2\AccessToken::post() 81 | * @covers OAuth2\AccessToken::put() 82 | * @covers OAuth2\AccessToken::delete() 83 | */ 84 | public function testRequest() 85 | { 86 | // header mode 87 | $this->accessToken->options['mode'] = 'header'; 88 | foreach (array('GET', 'POST', 'PUT', 'DELETE') as $verb) { 89 | // sends the token in the Authorization header for a {$verb} request 90 | $this->assertContains($this->token, $this->accessToken->request($verb, '/token/header')->body()); 91 | } 92 | 93 | // query mode 94 | $this->accessToken->options['mode'] = 'query'; 95 | foreach (array('GET', 'POST', 'PUT', 'DELETE') as $verb) { 96 | // sends the token in the query params for a {$verb} request 97 | $this->assertEquals($this->token, $this->accessToken->request($verb, '/token/query')->body()); 98 | } 99 | 100 | // body mode 101 | $this->accessToken->options['mode'] = 'body'; 102 | foreach (array('GET', 'POST', 'PUT', 'DELETE') as $verb) { 103 | // sends the token in the body for a {$verb} request 104 | $data = array_reverse(explode('=', $this->accessToken->request($verb, '/token/body')->body())); 105 | $this->assertEquals($this->token, $data[0]); 106 | } 107 | } 108 | 109 | /** 110 | * @covers OAuth2\AccessToken::expires() 111 | */ 112 | public function testExpires() 113 | { 114 | // should be false if expires_in is null 115 | $target = new \OAuth2\AccessToken($this->client, $this->token); 116 | $this->assertFalse($target->expires()); 117 | 118 | // should be true if there is an expires_in 119 | $target = new \OAuth2\AccessToken($this->client, $this->token, array('refresh_token' => 'abaca', 'expires_in' => 600)); 120 | $this->assertTrue($target->expires()); 121 | } 122 | 123 | /** 124 | * @covers OAuth2\AccessToken::isExpired() 125 | */ 126 | public function testIsExpired() 127 | { 128 | // should be false if there is no expires_in or expires_at 129 | $target = new \OAuth2\AccessToken($this->client, $this->token); 130 | $this->assertFalse($target->isExpired()); 131 | 132 | // should be false if expires_in is in the future 133 | $target = new \OAuth2\AccessToken($this->client, $this->token, array('refresh_token' => 'abaca', 'expires_in' => 10800)); 134 | $this->assertFalse($target->isExpired()); 135 | } 136 | 137 | /** 138 | * @covers OAuth2\AccessToken::refresh() 139 | */ 140 | public function testRefresh() 141 | { 142 | // returns a refresh token with appropriate values carried over 143 | $target = new \OAuth2\AccessToken($this->client, $this->token, array('refresh_token' => 'abaca', 'expires_in' => 600, 'param_name' => 'o_param')); 144 | $refreshed = $target->refresh(); 145 | $this->assertEquals($refreshed->getClient(), $target->getClient()); 146 | $this->assertEquals($refreshed->options['param_name'], $target->options['param_name']); 147 | } 148 | 149 | /** 150 | * Intercept all OAuth2\Client::getResponse() calls and mock their responses 151 | */ 152 | public function mockGetResponse() 153 | { 154 | // retrieve args 155 | $args = func_get_args(); 156 | 157 | // create response based on mode 158 | switch ($args[0]->getPath()) { 159 | case '/token/header': 160 | $body = sprintf($this->accessToken->options['header_format'], $this->accessToken->getToken()); 161 | return new \OAuth2\Response(new \GuzzleHttp\Message\Response(200, array(), \GuzzleHttp\Stream\Stream::factory($body))); 162 | break; 163 | 164 | case '/token/query': 165 | return new \OAuth2\Response(new \GuzzleHttp\Message\Response(200, array(), \GuzzleHttp\Stream\Stream::factory($this->accessToken->getToken()))); 166 | break; 167 | 168 | case '/token/body': 169 | $body = $this->accessToken->options['param_name'] . '=' . $this->accessToken->getToken(); 170 | return new \OAuth2\Response(new \GuzzleHttp\Message\Response(200, array(), \GuzzleHttp\Stream\Stream::factory($body))); 171 | break; 172 | 173 | case '/oauth/token': 174 | return new \OAuth2\Response(new \GuzzleHttp\Message\Response(200, ['Content-Type' => 'application/json'], \GuzzleHttp\Stream\Stream::factory($this->refreshBody))); 175 | break; 176 | } 177 | } 178 | 179 | /** 180 | * Assert for token initialization 181 | */ 182 | private function assertInitializeToken($target) { 183 | $this->assertEquals($this->token, $target->getToken()); 184 | $this->assertTrue($target->expires()); 185 | $this->assertArrayHasKey('foo', $target->getParams()); 186 | $this->assertEquals('bar', $target->getParam('foo')); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/OAuth2/AccessToken.php: -------------------------------------------------------------------------------- 1 | null // string The refresh_token value 58 | , 'expires_in' => null // integer The number of seconds in which the AccessToken will expire 59 | , 'mode' => 'header' // string The transmission mode of the Access Token parameter value one of 'header', 'body' or 'query' 60 | , 'header_format' => 'Bearer %s' // string The string format to use for the Authorization header 61 | , 'param_name' => 'bearer_token' // string he parameter name to use for transmission of the Access Token value in 'body' or 'query' transmission mode 62 | ), $opts); 63 | 64 | // Setting class attributes 65 | $this->client = $client; 66 | $this->token = $token; 67 | foreach (array('refresh_token', 'expires_in') as $arg) { 68 | // camelize arg 69 | $camelizedArg = lcfirst(str_replace(" ", "", ucwords(strtr($arg, "_-", " ")))); 70 | 71 | // set property 72 | $this->$camelizedArg = $opts[$arg]; 73 | unset($opts[$arg]); 74 | } 75 | 76 | $this->options = array( 77 | 'mode' => $opts['mode'] 78 | , 'header_format' => $opts['header_format'] 79 | , 'param_name' => $opts['param_name'] 80 | ); 81 | unset($opts['mode'], $opts['header_format'], $opts['param_name']); 82 | 83 | $this->params = $opts; 84 | } 85 | 86 | /** 87 | * client getter 88 | * 89 | * @return OAuth2\Client 90 | */ 91 | public function getClient() 92 | { 93 | return $this->client; 94 | } 95 | 96 | /** 97 | * expiresIn getter 98 | * 99 | * @return mixed 100 | */ 101 | public function getExpiresIn() 102 | { 103 | return $this->expiresIn; 104 | } 105 | 106 | /** 107 | * params getter 108 | * 109 | * @return array 110 | */ 111 | public function getParams() 112 | { 113 | return $this->params; 114 | } 115 | 116 | /** 117 | * param getter 118 | * 119 | * @return mixed 120 | */ 121 | public function getParam($key) 122 | { 123 | return isset($this->params[$key]) ? $this->params[$key] : null; 124 | } 125 | 126 | /** 127 | * refreshToken getter 128 | * 129 | * @return mixed 130 | */ 131 | public function getRefreshToken() 132 | { 133 | return $this->refreshToken; 134 | } 135 | 136 | /** 137 | * token getter 138 | * 139 | * @return mixed 140 | */ 141 | public function getToken() 142 | { 143 | return $this->token; 144 | } 145 | 146 | /** 147 | * Whether or not the token expires 148 | * 149 | * @return boolean 150 | */ 151 | public function expires() 152 | { 153 | return !is_null($this->expiresIn); 154 | } 155 | 156 | /** 157 | * Whether or not the token is expired 158 | * 159 | * @return boolean 160 | */ 161 | public function isExpired() 162 | { 163 | return $this->expires() && ($this->expiresIn === 0); 164 | } 165 | 166 | /** 167 | * Make a request with the Access Token 168 | * 169 | * @param string $verb The HTTP request method 170 | * @param string $path The HTTP URL path of the request 171 | * @param array $opts The options to make the request with 172 | * @see Client::sendRequest 173 | */ 174 | public function request($verb, $path, $opts = array()) 175 | { 176 | // Set parse mode 177 | $parseMode = 'automatic'; 178 | if (isset($opts['parse'])) { 179 | $parseMode = $opts['parse']; 180 | unset($opts['parse']); 181 | } 182 | 183 | // Set token 184 | $opts = $this->setToken($opts); 185 | 186 | // Make request and return response 187 | $request = $this->client->createRequest($verb, $path, $opts); 188 | return $this->client->getResponse($request, $parseMode); 189 | } 190 | 191 | /** 192 | * Make a GET request with the Access Token 193 | * 194 | * @see request 195 | */ 196 | public function get($path, $opts = array()) 197 | { 198 | return $this->request('GET', $path, $opts); 199 | } 200 | 201 | /** 202 | * Make a POST request with the Access Token 203 | * 204 | * @see request 205 | */ 206 | public function post($path, $opts = array()) 207 | { 208 | return $this->request('POST', $path, $opts); 209 | } 210 | 211 | /** 212 | * Make a PUT request with the Access Token 213 | * 214 | * @see request 215 | */ 216 | public function put($path, $opts = array()) 217 | { 218 | return $this->request('PUT', $path, $opts); 219 | } 220 | 221 | /** 222 | * Make a DELETE request with the Access Token 223 | * 224 | * @see request 225 | */ 226 | public function delete($path, $opts = array()) 227 | { 228 | return $this->request('DELETE', $path, $opts); 229 | } 230 | 231 | /** 232 | * Refreshes the current Access Token 233 | * 234 | * @param array $params 235 | * @return \OAuth2\AccessToken $new_token 236 | */ 237 | public function refresh($params = array()) 238 | { 239 | if (!$this->refreshToken) { 240 | throw new \ErrorException("A refresh_token is not available"); 241 | } 242 | 243 | $params = array_merge($params, array( 244 | 'grant_type' => 'refresh_token' 245 | , 'refresh_token' => $this->refreshToken 246 | )); 247 | 248 | $newToken = $this->client->getToken($params); 249 | $newToken->options = $this->options; 250 | return $newToken; 251 | } 252 | 253 | 254 | private function setToken($opts) 255 | { 256 | switch ($this->options['mode']) { 257 | case 'header': 258 | $opts['headers'] = isset($opts['headers']) ? $opts['headers'] : array(); 259 | $opts['headers']['Authorization'] = sprintf($this->options['header_format'], $this->token); 260 | break; 261 | 262 | case 'query': 263 | $opts['query'] = isset($opts['query']) ? $opts['query'] : array(); 264 | $opts['query'][$this->options['param_name']] = $this->token; 265 | break; 266 | 267 | case 'body': 268 | $opts['body'] = isset($opts['body']) ? $opts['body'] : ''; 269 | $opts['body'] .= "{$this->options['param_name']}={$this->token}"; 270 | break; 271 | 272 | default: 273 | throw new \ErrorException("invalid 'mode' option of {$this->options['mode']}"); 274 | break; 275 | } 276 | 277 | return $opts; 278 | } 279 | } 280 | 281 | -------------------------------------------------------------------------------- /tests/OAuth2/Tests/ClientTest.php: -------------------------------------------------------------------------------- 1 | client = $this->getClientStub('abc', 'def', array('site' => 'https://api.example.com')); 24 | } 25 | 26 | protected function tearDown() 27 | { 28 | unset($this->client); 29 | } 30 | 31 | /** 32 | * @covers OAuth2\Client::__construct() 33 | */ 34 | public function testConstructorBuildsClient() 35 | { 36 | // client id and secret should be assigned 37 | $this->assertEquals('abc', $this->client->getId()); 38 | $this->assertEquals('def', $this->client->getSecret()); 39 | 40 | // client site should be assigned 41 | $this->assertEquals(['base_url' => 'https://api.example.com'], $this->client->site); 42 | 43 | // connection baseUrl should be assigned 44 | $this->assertEquals('https://api.example.com', $this->client->connection->getBaseUrl()); 45 | 46 | // exceptions in request_opts should be true 47 | $this->assertTrue($this->client->options['request_opts']['exceptions']); 48 | 49 | // allows true/false exceptions in request_opts 50 | $client = new \OAuth2\Client('abc', 'def', [ 51 | 'site' => 'https://api.example.com' 52 | , 'request_opts' => [ 'exceptions' => false ] 53 | ]); 54 | $this->assertFalse($client->options['request_opts']['exceptions']); 55 | $client = new \OAuth2\Client('abc', 'def', [ 56 | 'site' => 'https://api.example.com' 57 | , 'request_opts' => [ 'exceptions' => true ] 58 | ]); 59 | $this->assertTrue($client->options['request_opts']['exceptions']); 60 | 61 | // allow GET/POST for token_method option 62 | $client = new \OAuth2\Client('abc', 'def', array('site' => 'https://api.example.com', 'token_method' => 'GET')); 63 | $this->assertEquals('GET', $client->options['token_method']); 64 | $client = new \OAuth2\Client('abc', 'def', array('site' => 'https://api.example.com', 'token_method' => 'POST')); 65 | $this->assertEquals('POST', $client->options['token_method']); 66 | } 67 | 68 | /** 69 | * @covers OAuth2\Client::authorize_url() 70 | * @covers OAuth2\Client::token_url() 71 | */ 72 | public function testUrlsEnpoints() 73 | { 74 | foreach (array('authorize', 'token') as $urlType) { 75 | // {$url_type}_url should default to /oauth/{$url_type} 76 | $this->assertEquals("https://api.example.com/oauth/{$urlType}", call_user_func(array($this->client, "{$urlType}Url"))); 77 | 78 | // {$url_type}_url should be settable via the {$url_type}_url option 79 | $this->client->options["{$urlType}_url"] = '/oauth/custom'; 80 | $this->assertEquals("https://api.example.com/oauth/custom", call_user_func(array($this->client, "{$urlType}Url"))); 81 | 82 | // allows a different host than the site 83 | $this->client->options["{$urlType}_url"] = 'https://api.foo.com/oauth/custom'; 84 | $this->assertEquals("https://api.foo.com/oauth/custom", call_user_func(array($this->client, "{$urlType}Url"))); 85 | } 86 | } 87 | 88 | /** 89 | * @covers OAuth2\Client::getResponse() 90 | */ 91 | public function testGetResponse() 92 | { 93 | // works with a null response body 94 | $request = $this->client->createRequest('GET', '/empty_get'); 95 | $this->assertEmpty((string) $this->client->getResponse($request)->body()); 96 | 97 | // returns on a successful response body 98 | $request = $this->client->createRequest('GET', '/success'); 99 | $response = $this->client->getResponse($request); 100 | $this->assertEquals('yay', $response->body()); 101 | $this->assertEquals(200, $response->status()); 102 | $headers = $response->headers(); 103 | $this->assertCount(1, $headers); 104 | $this->assertArrayHasKey('Content-Type', $headers); 105 | $this->assertEquals(array('text/awesome'), $headers['Content-Type']); 106 | 107 | // posts a body 108 | $request = $this->client->createRequest('POST', '/reflect', ['body' => 'foo=bar']); 109 | $response = $this->client->getResponse($request); 110 | $this->assertEquals('foo=bar', $response->body()); 111 | 112 | // follows redirect properly 113 | $request = $this->client->createRequest('GET', '/redirect'); 114 | $response = $this->client->getResponse($request); 115 | $this->assertEquals('yay', $response->body()); 116 | $this->assertEquals(200, $response->status()); 117 | $headers = $response->headers(); 118 | $this->assertCount(1, $headers); 119 | $this->assertArrayHasKey('Content-Type', $headers); 120 | $this->assertEquals(array('text/awesome'), $headers['Content-Type']); 121 | 122 | // redirects using GET on a 303 123 | $request = $this->client->createRequest('POST', '/redirect', ['body' => 'foo=bar']); 124 | $response = $this->client->getResponse($request); 125 | $this->assertEquals('', $response->body()); 126 | $this->assertEquals(200, $response->status()); 127 | 128 | // obeys the max_redirects option 129 | $request = $this->client->createRequest('GET', '/redirect', [ 'allow_redirects' => false ]); 130 | $response = $this->client->getResponse($request); 131 | $this->assertEquals(302, $response->status()); 132 | 133 | // returns if raise_errors is false 134 | $this->client->options['request_opts']['exceptions'] = false; 135 | $request = $this->client->createRequest('GET', '/unauthorized'); 136 | $response = $this->client->getResponse($request); 137 | $this->assertEquals(401, $response->status()); 138 | $headers = $response->headers(); 139 | $this->assertCount(1, $headers); 140 | $this->assertArrayHasKey('Content-Type', $headers); 141 | $this->assertEquals(array('application/json'), $headers['Content-Type']); 142 | $this->assertNotNull($response->error); 143 | 144 | // test if exception are thrown when raise_errors is true 145 | $this->client->options['request_opts']['exceptions'] = true; 146 | foreach (array('/unauthorized', '/conflict', '/error') as $errorPath) { 147 | $request = $this->client->createRequest('GET', $errorPath); 148 | 149 | // throw OAuth\Error on error response to path {$errorPath} 150 | $this->setExpectedException('\OAuth2\Error'); 151 | $this->client->getResponse($request); 152 | } 153 | 154 | // parses OAuth2 standard error response 155 | try { 156 | $request = $this->client->createRequest('GET', '/error'); 157 | $this->client->getResponse($request); 158 | } catch (\OAuth2\Error $e) { 159 | $this->assertEquals($this->errorValue, $e->getCode()); 160 | $this->assertEquals($this->errorDescriptionValue, $e->getDescription()); 161 | } 162 | 163 | // provides the response in the Exception 164 | try { 165 | $request = $this->client->createRequest('GET', '/error'); 166 | $this->client->getResponse($request); 167 | } catch (\OAuth2\Error $e) { 168 | $this->assertNotNull($e->getResponse()); 169 | } 170 | } 171 | 172 | /** 173 | * @covers OAuth2\Client::auth_code() 174 | */ 175 | public function testAuthCodeInstatiation() 176 | { 177 | // auth_code() should instantiate a AuthCode strategy with this client 178 | $this->assertInstanceOf("\OAuth2\Strategy\AuthCode", $this->client->authCode()); 179 | } 180 | 181 | /** 182 | * Intercept all OAuth2\Client::getResponse() calls and mock their responses 183 | */ 184 | public function mockGetResponse() 185 | { 186 | // retrieve arguments 187 | $args = func_get_args(); 188 | 189 | // map routes 190 | $map = array(); 191 | $map['GET']['/success'] = array('status' => 200, 'headers' => array('Content-Type' => 'text/awesome'), 'body' => 'yay'); 192 | $map['GET']['/reflect'] = array('status' => 200, 'headers' => array(), 'body' => $args[0]->getBody()); 193 | $map['POST']['/reflect'] = array('status' => 200, 'headers' => array(), 'body' => $args[0]->getBody()); 194 | $map['GET']['/unauthorized'] = array('status' => 401, 'headers' => array('Content-Type' => 'application/json'), 'body' => json_encode(array('error' => $this->errorValue, 'error_description' => $this->errorDescriptionValue))); 195 | $map['GET']['/conflict'] = array('status' => 409, 'headers' => array('Content-Type' => 'text/plain'), 'body' => 'not authorized'); 196 | $map['GET']['/redirect'] = array('status' => 302, 'headers' => array('Content-Type' => 'text/plain', 'location' => '/success'), 'body' => ''); 197 | $map['POST']['/redirect'] = array('status' => 303, 'headers' => array('Content-Type' => 'text/plain', 'location' => '/reflect'), 'body' => ''); 198 | $map['GET']['/error'] = array('status' => 500, 'headers' => array(), 'body' => ''); 199 | $map['GET']['/empty_get'] = array('status' => 200, 'headers' => array(), 'body' => ''); 200 | 201 | // match response 202 | $response = $map[$args[0]->getMethod()][$args[0]->getPath()]; 203 | 204 | // wrap response in an OAuth2\Response object 205 | $response = new \OAuth2\Response(new \GuzzleHttp\Message\Response($response['status'], $response['headers'], \GuzzleHttp\Stream\Stream::factory($response['body'])), $args[1]); 206 | 207 | // handle response 208 | if (in_array($response->status(), range(200, 299))) { 209 | return $response; 210 | } else if (in_array($response->status(), range(300, 399))) { 211 | // Increment redirect count 212 | $this->client->options['redirect_count'] = isset($this->client->options['redirect_count']) ? $this->client->options['redirect_count'] : 0; 213 | $this->client->options['redirect_count'] += 1; 214 | if ($this->client->options['redirect_count'] > $args[0]->getConfig()['redirect']['max']) { 215 | return $response; 216 | } 217 | 218 | // Retrieve data 219 | $method = ($response->status() === 303) ? 'GET' : $args[0]->getMethod(); 220 | $headers = $response->headers(); 221 | $location = $headers['location']; 222 | 223 | // Redirect request 224 | $request = $this->client->createRequest($method, $location[0], [ 'body' => $response->body() ]); 225 | return $this->client->getResponse($request); 226 | } else if (in_array($response->status(), range(400, 599))) { 227 | $e = new \OAuth2\Error($response); 228 | if ($args[0]->getConfig()['exceptions'] || $this->client->options['request_opts']['exceptions']) { 229 | throw $e; 230 | } 231 | $response->error = $e; 232 | return $response; 233 | } else { 234 | throw new \OAuth2\Error($response); 235 | } 236 | } 237 | } 238 | --------------------------------------------------------------------------------