rpicms

A CMS for the Raspberry Pi
git clone git://archive.git.mtrnord.blog/RpicmsTeam/rpicms.git
Log | Files | Refs | README | LICENSE

README.md (14940B)


      1 [![Build Status](https://travis-ci.org/fkooman/php-oauth-client.png?branch=master)](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