README.md (14940B)
1 [](https://travis-ci.org/fkooman/php-oauth-client) 2 3 # Introduction 4 This project provides an OAuth 2.0 "Authorization Code Grant" client as 5 described in RFC 6749, section 4.1. 6 7 The client can be controlled through a PHP API that is used from the 8 application trying to access an OAuth 2.0 protected resource server. 9 10 # Features 11 The following features are supported: 12 13 * "Authorization Code Grant" Profile 14 * Refresh Tokens 15 16 # License 17 Licensed under the GNU Lesser General Public License as published by the Free 18 Software Foundation, either version 3 of the License, or (at your option) any 19 later version. 20 21 https://www.gnu.org/licenses/lgpl.html 22 23 This roughly means that if you write some PHP application that uses this client 24 you do not need to release your application under (L)GPL as well. Refer to the 25 license for the exact details. 26 27 # Application Integration 28 If you want to integrate this OAuth client in your application you need to 29 answer some questions: 30 31 * Where am I going to store the access tokens? 32 * How do I make an endpoint URL available in my application that can be used as 33 a redirect URL for the callback from the authorization server? 34 35 Next to this you need OAuth client credentials from the authorization server 36 and REST API documentation from the service you want to connect to. You for 37 instance need to know the `authorize_endpoint`, the `token_endpoint`, the 38 `client_id` and `client_secret`. 39 40 As for storing access tokens, this library includes two backends. One for 41 storing the tokens in a database (using the PHP PDO abstraction layer) and one 42 for storing them in the user session. The first one requires some setup, the 43 second one is very easy to use (no configuration) but will not allow the client 44 to access data at the resource server without the session data being available. 45 A more robust implementation would use the PDO backed storage. For testing 46 purposes or very simple setups the session implementation makes the most sense. 47 48 For accessing the resource service a Guzzle plugin is available that will help 49 you with that. 50 51 The sections below will walk through all the steps you need in order to get the 52 client working. 53 54 ## Example 55 In addition to this, a full example is available in the `example` directory. 56 This includes `index.php` that does the token request and requests the data. 57 Also a `callback.php` is included to show how to use the `Callback` API. Next 58 to this a `composer.json` is included for use with Composer. 59 60 ## Composer 61 In order to easily integrate with your application it is recommended to use 62 Composer to install the dependencies. You need to install two libraries to 63 use this library: 64 65 * `fkooman/php-oauth-client` 66 * `fkooman/guzzle-bearer-auth-plugin` 67 68 Below is a simple example `composer.json` file you could use: 69 70 { 71 "name": "fkooman/my-demo-oauth-app", 72 "require": { 73 "fkooman/guzzle-bearer-auth-plugin": "dev-master", 74 "fkooman/php-oauth-client": "dev-master" 75 } 76 } 77 78 ## Client Configuration 79 You can create an client configuration object as shown below. You can fetch 80 this from a configuration file in your application if desired. Below is an 81 example of the generic `ClientConfig` class: 82 83 $clientConfig = new ClientConfig( 84 array( 85 "authorize_endpoint" => "http://localhost/oauth/php-oauth/authorize.php", 86 "client_id" => "foo", 87 "client_secret" => "foobar", 88 "token_endpoint" => "http://localhost/oauth/php-oauth/token.php", 89 ) 90 ); 91 92 There is also a `GoogleClientConfig` class that you can use with Google's 93 `client_secrets.json` file format: 94 95 // Google 96 $googleClientConfig = new GoogleClientConfig( 97 json_decode(file_get_contents("client_secrets.json"), true) 98 ); 99 100 The Google class also sets some Google specific options to deal with some 101 specification violations. Configuration options to deal with specification 102 violating services are: 103 104 * `allow_null_expires_in` in case the OAuth 2.0 AS returns `"expires_in": null` 105 this setting removes the `expires_in` field when it is `null` so it falls 106 back to assuming the token is valid indefinitely. Set it to `true` to enable 107 this configuration option, defaults to `false`. AS with this behavior: 108 SurveyMonkey. 109 * `default_token_type` in case the OAuth 2.0 AS omits the `token_type` field 110 altogether. This allows you to set the `token_type`. For example you can set 111 it to `bearer` if you know that is the type the AS returns. AS with this 112 behavior: Salesforce. 113 * `credentials_in_request_body` in case the OAuth 2.0 AS does not accept Basic 114 authentication on the token endpoint. This will force the client to use 115 `client_id` and `client_secret` POST body fields to specify the credentials. 116 This option will also make it possible to allow for the `client_id` to have 117 a colon (`:`) in it. AS with this behavior: Google, SurveyMonkey, GitHub. 118 * `default_server_scope` in case the server returns a specification violating 119 empty string as `scope`. This will override the scope value allowing you to 120 set one. It will *ONLY* be set when the server scope is an empty string, not 121 as a default in all situations! AS with this behavior: Nationbuilder. 122 * `use_redirect_uri_on_refresh_token_request` in case the server requires you 123 to also provide the redirect_uri parameter on a refresh_token request. AS 124 with this behavior: Nationbuilder. 125 * `use_comma_separated_scope` when the AS sends the scope response as a comma 126 separated value instead of space separated. AS with this behavior: GitHub. 127 * `use_array_scope` when the AS sends the scope as an array instead of a space 128 separated string. 129 * `allow_string_expires_in` some AS returns the expires_in value as a string 130 instead of a numerical value. Setting this will cast the string to 131 number. 132 133 ## Initializing the API 134 Now you can initialize the `Api` object: 135 136 $api = new Api("foo", $clientConfig, new SessionStorage(), new \Guzzle\Http\Client()); 137 138 In this example we use the `SessionStorage` token storage backend. This is used 139 to keep the obtained tokens in the user session. For testing purposes this is 140 sufficient, for production deployments you will want to use the `PdoStorage` 141 backend instead, see below. 142 143 You also need to provide an instance of Guzzle which is a HTTP client used to 144 exchange authorization codes for access tokens, or use a refresh token to 145 obtain a new access token. 146 147 ## Requesting Tokens 148 In order to request tokens you need to use two methods: `Api::getAccessToken()` 149 and `Api::getAuthorizeUri()`. The first one is used to see if there is already 150 a token available, the second to obtain an URL to which you have to redirect 151 the browser from your application. The example below will show you how to use 152 these methods. 153 154 Before you can call these methods you need to create a `Context` object to 155 specify for which user you are requesting this access token and what the scope 156 is you want to request at the authorization server. 157 158 $context = new Context("john.doe@example.org", array("read")); 159 160 This means that you will request a token bound to `john.doe@example.org` with 161 the scope `read`. The user you specify here is typically the user identifier 162 you use in *your* application that wants to integrate with the OAuth 2.0 163 protected resource. At your service the user can for example be 164 `john.doe@example.org`. This identifier is in no way related to the identity 165 of the user at the remote service, it is just used for book keeping the 166 access tokens. If you do not want to request any particular scope you can use 167 `array()`. 168 169 Now you can see if an access token is already available: 170 171 $accessToken = $api->getAccessToken($context); 172 173 This call returns `false` if no access token is available for this user and 174 scope and none could be obtained through the backchannel using a refresh token. 175 This means that there never was a token or it expired. The token can still be 176 revoked, but we cannot see that right now, we'll find that out when we try to 177 use it later. 178 179 Assuming the `getAccessToken($context)` call returns `false`, i.e.: there was 180 no token, we have to obtain authorization: 181 182 if (false === $accessToken) { 183 /* no valid access token available, go to authorization server */ 184 header("HTTP/1.1 302 Found"); 185 header("Location: " . $api->getAuthorizeUri($context)); 186 exit; 187 } 188 189 This is the simplest way if your application is not using any framework. 190 If your application uses a framework you can probably use that to do "proper" 191 redirect without setting the HTTP headers yourself. You should use this! 192 193 After this, the flow of this script ends and the user is redirected to the 194 authorization server. Once there, the user accepts the client request and is 195 redirected back to the redirection URL you registered at the OAuth 2.0 service 196 provider. You also need to put some code at this callback location, see the 197 next section below. 198 199 Assuming you already had an access token, i.e.: the response from 200 `Api::getAccessToken()` was not `false` you can now try to get the resource. 201 This example uses Guzzle as well: 202 203 $apiUrl = 'http://www.example.org/resource'; 204 205 try { 206 $client = new Client(); 207 $bearerAuth = new BearerAuth($accessToken->getAccessToken()); 208 $client->addSubscriber($bearerAuth); 209 $response = $client->get($apiUrl)->send(); 210 211 header("Content-Type: application/json"); 212 echo $response->getBody(); 213 } catch (BearerErrorResponseException $e) { 214 if ("invalid_token" === $e->getBearerReason()) { 215 // the token we used was invalid, possibly revoked, we throw it away 216 $api->deleteAccessToken($context); 217 $api->deleteRefreshToken($context); 218 219 /* no valid access token available, go to authorization server */ 220 header("HTTP/1.1 302 Found"); 221 header("Location: " . $api->getAuthorizeUri($context)); 222 exit; 223 } 224 throw $e; 225 } 226 227 Pay special attention to the `BearerErrorResponseException` where both the 228 access token and refresh token are deleted when the access token does not work. 229 If that happens, the browser is redirected like in the case when there was no 230 token yet. 231 232 ## Handling the Callback 233 The above situation assumed you already had a valid access token. If you didn't 234 you got redirected to the authorization server where you had to accept the 235 request for access to your data. Assuming that all went well you will be 236 redirected back to the redirection URI you registered at the OAuth 2.0 service. 237 238 The `Callback` class is very similar to the `Api` class. We assume you 239 also create the `ClientConfig` object here, like in the `Api` case. The 240 contents of this file are assumed to be in `callback.php`. 241 242 try { 243 $cb = new Callback("foo", $clientConfig, new SessionStorage(), new \Guzzle\Http\Client()); 244 $cb->handleCallback($_GET); 245 246 header("HTTP/1.1 302 Found"); 247 header("Location: http://www.example.org/index.php"); 248 } catch (AuthorizeException $e) { 249 // this exception is thrown by Callback when the OAuth server returns a 250 // specific error message for the client, e.g.: the user did not authorize 251 // the request 252 echo sprintf("ERROR: %s, DESCRIPTION: %s", $e->getMessage(), $e->getDescription()); 253 } catch (Exception $e) { 254 // other error, these should never occur in the normal flow 255 echo sprintf("ERROR: %s", $e->getMessage()); 256 } 257 258 This is all that is needed here. The authorization code will be extracted from 259 the callback URL and used to obtain an access token. The access token will be 260 stored in the token storage, here `SessionStorage` and the browser will be 261 redirected back to the page where the `Api` calls are made, here `index.php`. 262 263 # Token Storage 264 You can store the tokens either in `SessionStorage` or `PdoStorage`. The first 265 one is already demonstrated above and requires no further configuration, it 266 just works out of the box. 267 268 $tokenStorage = new SessionStorage(); 269 270 The PDO backend requires you specifying the database you want to use: 271 272 $db = new PDO("sqlite:/path/to/db/client.sqlite"); 273 $tokenStorage = new PdoStorage($db); 274 275 In both cases you can use `$tokenStorage` in the constructor where before we 276 put `new SessionStorage()` there directly. See the PHP PDO documentation on how 277 to specify other databases. 278 279 Please note that if you use SQLite, please note that the *directory* you write 280 the file to needs to be writable to the web server as well! 281 282 If you want to use a table prefix, please use the second parameter of the 283 constructor to set the prefix, for instance: 284 285 $tokenStorage = new PdoStorage($db, "foo_"); 286 287 To see the database schema you need to import in your database you can use the 288 script in the `bin` directory, the first parameter is the prefix you want to 289 use for your tables: 290 291 $ php bin/php-oauth-client-create-tables foo_ 292 293 If you specify no parameter no prefix is assumed and the tables to be created 294 are shown without prefix. 295 296 # Logging 297 In order to log all the requests the OAuth library makes to the token endpoint 298 it is possible to use e.g. the Monolog adapter for this. Below is an example 299 on how to do this. For production systems you may want to integrate with your 300 own logging framework, set the appropriate log level, e.g. only log on errors. 301 302 So instead of just using 303 304 new Client() 305 306 You can use the following snippet: 307 308 use Monolog\Logger; 309 use Monolog\Handler\StreamHandler; 310 311 use Guzzle\Plugin\Log\LogPlugin; 312 use Guzzle\Log\MessageFormatter; 313 use Guzzle\Log\MonologLogAdapter; 314 315 /* create the log channel */ 316 $log = new Logger('my-app'); 317 $log->pushHandler(new StreamHandler(sprintf("%s/data/client.log", __DIR__), Logger::DEBUG)); 318 $logPlugin = new LogPlugin(new MonologLogAdapter($log), MessageFormatter::DEBUG_FORMAT); 319 320 $httpClient = new Client(); 321 $httpClient->addSubscriber($logPlugin); 322 323 Now you can feed the `$httpClient` to the `Api` and `Callback` classes and the 324 requests and responses including their bodies will be logged. 325 326 # Tests 327 In order to run the tests you can use [PHPUnit](http://phpunit.de). You can run 328 the tests like this: 329 330 $ php /path/to/phpunit.phar tests 331 332 from the directory. Make sure you first run 333 `php /path/to/composer.phar install` before running the tests. By default the 334 SQLite PDO driver is used for running database tests. If you want to use 335 another database to test against, copy the `phpunit.xml.dist` file to 336 `phpunit.xml` and modify the configuration. For example to use MySQL, 337 configure it like this: 338 339 <php> 340 <var name="DB_DSN" value="mysql:dbname=oauth;host=localhost" /> 341 <var name="DB_USER" value="foo" /> 342 <var name="DB_PASSWD" value="bar" /> 343 </php> 344