create an API to retrieve emails and set new clients, add very rudimentary client...
authorRobert Kaiser <kairo@kairo.at>
Sat, 29 Oct 2016 16:29:39 +0000 (18:29 +0200)
committerRobert Kaiser <kairo@kairo.at>
Sat, 29 Oct 2016 16:29:39 +0000 (18:29 +0200)
.htaccess [new file with mode: 0644]
api.php [new file with mode: 0644]
authorize.php
authsystem.css
authutils.php-class
index.php
resource.php [deleted file]
server.inc.php

diff --git a/.htaccess b/.htaccess
new file mode 100644 (file)
index 0000000..da4a7f8
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,5 @@
+       RewriteEngine On
+       # Send calls to the PHP equivalents.
+       RewriteCond %{query_string} ^(.+) [NC]
+       RewriteRule ^(authorize|token|api)$ /$1.php?%1 [L,NE,PT]
+       RewriteRule ^(authorize|token|api)$ /$1.php [L,NE,PT]
diff --git a/api.php b/api.php
new file mode 100644 (file)
index 0000000..b3096d1
--- /dev/null
+++ b/api.php
@@ -0,0 +1,143 @@
+<?php
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Called e.g. as /api?access_token=...&whatever_api_parameters
+// access_token can be handed via GET or POST or an 'Authorization: Bearer' header.
+// Response is always JSON.
+
+// Include the common auth system files (including the OAuth2 Server object).
+require_once(__DIR__.'/authsystem.inc.php');
+
+$errors = $utils->checkForSecureConnection();
+
+if (!count($errors)) {
+  // Handle a request to a resource and authenticate the access token
+  $token_OK = $server->verifyResourceRequest(OAuth2\Request::createFromGlobals());
+  if (!$token_OK) {
+    $server->getResponse()->send();
+    exit();
+  }
+  $token = $server->getAccessTokenData(OAuth2\Request::createFromGlobals());
+  // API request successful, return requested resource.
+  if (array_key_exists('email', $_GET)) {
+    if ($token['scope'] == 'email') {
+      if (intval(@$token['user_id'])) {
+        $result = $db->prepare('SELECT `id`,`email` FROM `auth_users` WHERE `id` = :userid;');
+        $result->execute(array(':userid' => $token['user_id']));
+        $user = $result->fetch(PDO::FETCH_ASSOC);
+        if (!$user['id']) {
+          $utils->log('user_token_failure', 'token: '.$token['access_token']);
+          print(json_encode(array('error' => 'unknown_user',
+                                  'error_description' => 'The user the access token is connected to was not recognized.')));
+        }
+        else {
+          print(json_encode(array('success' => true, 'email' => $user['email'])));
+        }
+      }
+      else {
+        print(json_encode(array('error' => 'no_user',
+                                'error_description' => 'The access token is not connected to a user.')));
+      }
+    }
+    else {
+      print(json_encode(array('error' => 'insufficient_scope',
+                              'error_description' => 'The scope of the token you used in this API request is insufficient to access this resource.')));
+    }
+  }
+  elseif (array_key_exists('newclient', $_GET)) {
+    if ($token['scope'] == 'clientreg') {
+      if (intval(@$token['user_id'])) {
+        $result = $db->prepare('SELECT `id`,`email` FROM `auth_users` WHERE `id` = :userid;');
+        $result->execute(array(':userid' => $token['user_id']));
+        $user = $result->fetch(PDO::FETCH_ASSOC);
+        if (!$user['id']) {
+          $utils->log('user_token_failure', 'token: '.$token['access_token']);
+          print(json_encode(array('error' => 'unknown_user',
+                                  'error_description' => 'The user the access token is connected to was not recognized.')));
+        }
+        else {
+          if (in_array($user['email'], $utils->client_reg_email_whitelist)) {
+            if (strlen(@$_GET['client_id']) >= 5) {
+              $result = $db->prepare('SELECT `client_id`,`user_id` FROM `oauth_clients` WHERE `client_id` = :clientid;');
+              $result->execute(array(':clientid' => $_GET['client_id']));
+              $client = $result->fetch(PDO::FETCH_ASSOC);
+              if (!$client['client_id']) {
+                // Set new client ID.
+                $clientsecret = $utils->createClientSecret();
+                $result = $db->prepare('INSERT INTO `oauth_clients` (`client_id`, `client_secret`, `redirect_uri`, `scope`, `user_id`) VALUES (:clientid, :secret, :rediruri, :scope, :userid);');
+                if ($result->execute(array(':clientid' => $_GET['client_id'],
+                                           ':secret' => $clientsecret,
+                                           ':rediruri' => (strlen(@$_GET['redirect_uri']) ? $_GET['redirect_uri'] : ''),
+                                           ':scope' => (strlen(@$_GET['scope']) ? $_GET['scope'] : ''),
+                                           ':userid' => $user['id']))) {
+                  print(json_encode(array('success' => true, 'client_secret' => $clientsecret)));
+                }
+                else {
+                  $utils->log('client_save_failure', 'client: '.$client['client_id']);
+                  print(json_encode(array('error' => 'unexpected_save_failure',
+                                          'error_description' => 'Unexpectedly failed to save new client information.')));
+                }
+              }
+              elseif ($client['user_id'] == $user['id']) {
+                // The client ID was set by this user, set new secret and return.
+                $clientsecret = $utils->createClientSecret();
+                $result = $db->prepare('UPDATE `oauth_clients` SET `client_secret` = :secret WHERE `client_id` = :clientid;');
+                if (!$result->execute(array(':secret' => $clientsecret,':clientid' => $client['client_id']))) {
+                  $utils->log('client_save_failure', 'client: '.$client['client_id'].', new secret - '.$result->errorInfo()[2]);
+                  print(json_encode(array('error' => 'unexpected_save_failure',
+                                          'error_description' => 'Unexpectedly failed to save new secret.')));
+                }
+                else {
+                  if (strlen(@$_GET['redirect_uri'])) {
+                    $result = $db->prepare('UPDATE `oauth_clients` SET `redirect_uri` = :rediruri WHERE `client_id` = :clientid;');
+                    if (!$result->execute(array(':rediruri' => $_GET['redirect_uri'],':clientid' => $client['client_id']))) {
+                      $utils->log('client_save_failure', 'client: '.$client['client_id'].', new redirect_uri: '.$_GET['redirect_uri'].' - '.$result->errorInfo()[2]);
+                    }
+                  }
+                  if (strlen(@$_GET['scope'])) {
+                    $result = $db->prepare('UPDATE `oauth_clients` SET `scope` = :scope WHERE `client_id` = :clientid;');
+                    if (!$result->execute(array(':scope' => $_GET['scope'],':clientid' => $client['client_id']))) {
+                      $utils->log('client_save_failure', 'client: '.$client['client_id'].', new scope: '.$_GET['scope'].' - '.$result->errorInfo()[2]);
+                    }
+                  }
+                  print(json_encode(array('success' => true, 'client_secret' => $clientsecret)));
+                }
+              }
+              else {
+                print(json_encode(array('error' => 'client_id_used',
+                                        'error_description' => 'This client ID is in use by a different user.')));
+              }
+            }
+            else {
+              print(json_encode(array('error' => 'invalid_client_id_',
+                                      'error_description' => 'A client ID of at least 5 characters needs to be supplied.')));
+            }
+          }
+          else {
+            print(json_encode(array('error' => 'insufficient_privileges',
+                                    'error_description' => 'This user is not allowed to register new clients.')));
+          }
+        }
+      }
+      else {
+        print(json_encode(array('error' => 'no_user',
+                                'error_description' => 'The access token is not connected to a user.')));
+      }
+    }
+    else {
+      print(json_encode(array('error' => 'insufficient_scope',
+                              'error_description' => 'The scope of the token you used in this API request is insufficient to access this resource.')));
+    }
+  }
+  else {
+    print(json_encode(array('error' => 'invalid_resource',
+                            'error_description' => 'The resource requested from the API is unknown.')));
+  }
+}
+else {
+  print(json_encode(array('error' => 'insecure_connection',
+                          'error_description' => 'Your connection is insecure. The API can only be accessed on secure connections.')));
+}
+?>
index 4275254873b22fffd0dfaad35dd9490eff2050c4..33ffd09fc173be3807f574f6c27acb17b97e77d8 100644 (file)
@@ -6,7 +6,7 @@
 // Called e.g. as /authorize?response_type=code&client_id=testclient&state=f00bar&scope=email&redirect_uri=http%3A%2F%2Ffake.example.com%2F
 // This either redirects to the redirect URL with errors or success added as GET parameters,
 // or sends a HTML page asking for login / permission to scope (email is always granted in this system but not always for OAuth2 generically)
 // Called e.g. as /authorize?response_type=code&client_id=testclient&state=f00bar&scope=email&redirect_uri=http%3A%2F%2Ffake.example.com%2F
 // This either redirects to the redirect URL with errors or success added as GET parameters,
 // or sends a HTML page asking for login / permission to scope (email is always granted in this system but not always for OAuth2 generically)
-// or sends errors as a JSOn document (hopefully shouldn't but seen that in testing).
+// or sends errors as a JSON document (hopefully shouldn't but seen that in testing).
 
 // Include the common auth system files (including the OAuth2 Server object).
 require_once(__DIR__.'/authsystem.inc.php');
 
 // Include the common auth system files (including the OAuth2 Server object).
 require_once(__DIR__.'/authsystem.inc.php');
@@ -72,7 +72,7 @@ if (!count($errors)) {
     else {
       // Handle authorize request, forwarding code in GET parameters if the user has authorized your client.
       $is_authorized = (($_POST['authorized'] === 'yes') || ($request->query['scope'] == 'email'));
     else {
       // Handle authorize request, forwarding code in GET parameters if the user has authorized your client.
       $is_authorized = (($_POST['authorized'] === 'yes') || ($request->query['scope'] == 'email'));
-      $server->handleAuthorizeRequest($request, $response, $is_authorized);
+      $server->handleAuthorizeRequest($request, $response, $is_authorized, $user['id']);
       /* For testing only
       if ($is_authorized) {
         // this is only here so that you get to see your code in the cURL request. Otherwise, we'd redirect back to the client
       /* For testing only
       if ($is_authorized) {
         // this is only here so that you get to see your code in the cURL request. Otherwise, we'd redirect back to the client
index a3f192f0e0af2d83870dd475d8656ad57a5cf0a1..a164fb9543a1a9344dc3f33383201fed26bb30c6 100644 (file)
@@ -28,6 +28,22 @@ form.flat {
   padding: 0px;
 }
 
   padding: 0px;
 }
 
+table.border {
+ border-spacing: 0px;
+ border-collapse: collapse;
+ empty-cells: show;
+ border-left: 1px solid #336699;
+ border-top: 1px solid #336699;
+}
+table.border th, table.border td {
+ border-bottom: 1px solid #336699;
+ border-right: 1px solid #336699;
+}
+table.border td {
+ padding-left: 3px;
+ padding-right: 3px;
+}
+
 .small {
   font-size: 0.75em;
 }
 .small {
   font-size: 0.75em;
 }
index 4d5680a5264b4402bd80ffce5e9bcd37aa6b352a..dc06a7f7547ab17cabfce789fe2c1624de47b477 100755 (executable)
@@ -18,6 +18,9 @@ class AuthUtils {
   // public $running_on_localhost
   //   A boolean telling if the system is running on localhost (where https is not required).
   //
   // public $running_on_localhost
   //   A boolean telling if the system is running on localhost (where https is not required).
   //
+  // public $client_reg_email_whitelist
+  //   An array of emails that are whitelisted for registering clients.
+  //
   // private $pwd_cost
   //   The cost parameter for use with PHP password_hash function.
   //
   // private $pwd_cost
   //   The cost parameter for use with PHP password_hash function.
   //
@@ -46,6 +49,9 @@ class AuthUtils {
   // function createVerificationCode()
   //   Return a random acount/email verification code.
   //
   // function createVerificationCode()
   //   Return a random acount/email verification code.
   //
+  // function createClientSecret()
+  //   Return a random client secret.
+  //
   // function createTimeCode($session, [$offset], [$validity_minutes])
   //   Return a time-based code based on the key and ID of the given session.
   //     An offset can be given to create a specific code for verification, otherwise and offset will be generated.
   // function createTimeCode($session, [$offset], [$validity_minutes])
   //   Return a time-based code based on the key and ID of the given session.
   //     An offset can be given to create a specific code for verification, otherwise and offset will be generated.
@@ -82,6 +88,7 @@ class AuthUtils {
 
   public $db = null;
   public $running_on_localhost = false;
 
   public $db = null;
   public $running_on_localhost = false;
+  public $client_reg_email_whitelist = array('kairo@kairo.at', 'com@kairo.at');
   private $pwd_cost = 10;
   private $pwd_nonces = array();
 
   private $pwd_cost = 10;
   private $pwd_nonces = array();
 
@@ -166,6 +173,10 @@ class AuthUtils {
     return bin2hex(openssl_random_pseudo_bytes(512 / 8)); // Get 512 bits of randomness (128 byte hex string).
   }
 
     return bin2hex(openssl_random_pseudo_bytes(512 / 8)); // Get 512 bits of randomness (128 byte hex string).
   }
 
+  function createClientSecret() {
+    return bin2hex(openssl_random_pseudo_bytes(160 / 8)); // Get 160 bits of randomness (40 byte hex string).
+  }
+
   function createTimeCode($session, $offset = null, $validity_minutes = 10) {
     // Matches TOTP algorithms, see https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm
     $valid_seconds = intval($validity_minutes) * 60;
   function createTimeCode($session, $offset = null, $validity_minutes = 10) {
     // Matches TOTP algorithms, see https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm
     $valid_seconds = intval($validity_minutes) * 60;
index ee03d8b034c115a935d0913eab33b04edd138733..76e11d28fa6f47e4734fc1ebc2a5c74cab5a3b53 100644 (file)
--- a/index.php
+++ b/index.php
@@ -284,6 +284,39 @@ if (!count($errors)) {
       $errors[] = _('The password reset link you called is not valid. Possibly it has expired and you need to call the "Password forgotten?" function again.');
     }
   }
       $errors[] = _('The password reset link you called is not valid. Possibly it has expired and you need to call the "Password forgotten?" function again.');
     }
   }
+  elseif (array_key_exists('clients', $_GET)) {
+    $result = $db->prepare('SELECT `id`,`email` FROM `auth_users` WHERE `id` = :userid;');
+    $result->execute(array(':userid' => $session['user']));
+    $user = $result->fetch(PDO::FETCH_ASSOC);
+    if ($session['logged_in'] && $user['id']) {
+      if (array_key_exists('client_id', $_POST) && (strlen($_POST['client_id']) >= 5)) {
+        $clientid = $_POST['client_id'];
+        $clientsecret = $utils->createClientSecret();
+        $rediruri = strval(@$_POST['redirect_uri']);
+        $scope = strval(@$_POST['scope']);
+        $result = $db->prepare('INSERT INTO `oauth_clients` (`client_id`, `client_secret`, `redirect_uri`, `scope`, `user_id`) VALUES (:clientid, :secret, :rediruri, :scope, :userid);');
+        if (!$result->execute(array(':clientid' => $clientid,
+                                    ':secret' => $clientsecret,
+                                    ':rediruri' => $rediruri,
+                                    ':scope' => $scope,
+                                    ':userid' => $user['id']))) {
+          $utils->log('client_save_failure', 'client: '.$clientid);
+          $errors[] = 'Unexpectedly failed to save new client information. Please <a href="https://www.kairo.at/contact">contact KaiRo.at</a> and tell the team about this.';
+        }
+      }
+      if (!count($errors)) {
+        // List clients
+        $result = $db->prepare('SELECT `client_id`,`client_secret`,`redirect_uri`,`scope` FROM `oauth_clients` WHERE `user_id` = :userid;');
+        $result->execute(array(':userid' => $user['id']));
+        $clients = $result->fetchAll(PDO::FETCH_ASSOC);
+        if (!$clients) { $clients = array(); }
+        $pagetype = 'clientlist';
+      }
+    }
+    else {
+      $errors[] = _('This function is only available if you are logged in.');
+    }
+  }
   elseif (intval($session['user'])) {
     $result = $db->prepare('SELECT `id`,`email`,`verify_hash` FROM `auth_users` WHERE `id` = :userid;');
     $result->execute(array(':userid' => $session['user']));
   elseif (intval($session['user'])) {
     $result = $db->prepare('SELECT `id`,`email`,`verify_hash` FROM `auth_users` WHERE `id` = :userid;');
     $result->execute(array(':userid' => $session['user']));
@@ -376,6 +409,45 @@ if (!count($errors)) {
     }
     $submit = $litem->appendInputSubmit(_('Save password'));
   }
     }
     $submit = $litem->appendInputSubmit(_('Save password'));
   }
+  elseif ($pagetype == 'clientlist') {
+    $scopes = array('clientreg', 'email');
+    $form = $body->appendForm('?clients', 'POST', 'newclientform');
+    $form->setAttribute('id', 'clientform');
+    $tbl = $form->appendElement('table');
+    $tbl->setAttribute('class', 'clientlist border');
+    $thead = $tbl->appendElement('thead');
+    $trow = $thead->appendElement('tr');
+    $trow->appendElement('th', _('Client ID'));
+    $trow->appendElement('th', _('Client Secrect'));
+    $trow->appendElement('th', _('Redirect URI'));
+    $trow->appendElement('th', _('Scope'));
+    $trow->appendElement('th');
+    $tbody = $tbl->appendElement('tbody');
+    foreach ($clients as $client) {
+      $trow = $tbody->appendElement('tr');
+      $trow->appendElement('td', $client['client_id']);
+      $trow->appendElement('td', $client['client_secret']);
+      $trow->appendElement('td', $client['redirect_uri']);
+      $trow->appendElement('td', $client['scope']);
+      $trow->appendElement('td'); // Future: Delete link?
+    }
+    // Form fields for adding a new one.
+    $tfoot = $tbl->appendElement('tfoot');
+    $trow = $tfoot->appendElement('tr');
+    $cell = $trow->appendElement('td');
+    $inptxt = $cell->appendInputText('client_id', 80, 25, 'client_id');
+    $cell = $trow->appendElement('td'); // Empty, as secret will be generated.
+    $cell = $trow->appendElement('td');
+    $inptxt = $cell->appendInputText('redirect_uri', 500, 50, 'redirect_uri');
+    $cell = $trow->appendElement('td');
+    $select = $cell->appendElementSelect('scope');
+    foreach ($scopes as $scope) {
+      $select->appendElementOption($scope, $scope);
+    }
+    //$inptxt = $cell->appendInputText('scope', 100, 20, 'scope');
+    $cell = $trow->appendElement('td');
+    $submit = $cell->appendInputSubmit(_('Create'));
+  }
   elseif ($session['logged_in']) {
     if ($pagetype == 'reset_done') {
       $para = $body->appendElement('p', _('Your password has successfully been reset.'));
   elseif ($session['logged_in']) {
     if ($pagetype == 'reset_done') {
       $para = $body->appendElement('p', _('Your password has successfully been reset.'));
@@ -389,6 +461,10 @@ if (!count($errors)) {
     $ulist->setAttribute('class', 'flat');
     $litem = $ulist->appendElement('li');
     $link = $litem->appendLink('./?logout', _('Log out'));
     $ulist->setAttribute('class', 'flat');
     $litem = $ulist->appendElement('li');
     $link = $litem->appendLink('./?logout', _('Log out'));
+    if (in_array($user['email'], $utils->client_reg_email_whitelist)) {
+      $litem = $ulist->appendElement('li');
+      $link = $litem->appendLink('./?clients', _('Manage OAuth2 clients'));
+    }
     $litem = $ulist->appendElement('li');
     $litem->appendLink('./?reset', _('Set new password'));
   }
     $litem = $ulist->appendElement('li');
     $litem->appendLink('./?reset', _('Set new password'));
   }
diff --git a/resource.php b/resource.php
deleted file mode 100644 (file)
index 436f415..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-<?php
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-// Simple server based on https://bshaffer.github.io/oauth2-server-php-docs/cookbook
-
-// Include the common auth system files (including the OAuth2 Server object).
-require_once(__DIR__.'/authsystem.inc.php');
-
-// Handle a request to a resource and authenticate the access token
-if (!$server->verifyResourceRequest(OAuth2\Request::createFromGlobals())) {
-    $server->getResponse()->send();
-    die;
-}
-echo json_encode(array('success' => true, 'message' => 'You accessed my APIs!'));
-
-?>
index 6a82b50002b8197e47b273c331568ad5ce85a4c6..87b6535d4132b694d008bf3076caf794d8596963 100644 (file)
@@ -12,15 +12,25 @@ require_once('../oauth2-server-php/src/OAuth2/Autoloader.php');
 OAuth2\Autoloader::register();
 
 // $dsn is the Data Source Name for your database, for exmaple "mysql:dbname=my_oauth2_db;host=localhost"
 OAuth2\Autoloader::register();
 
 // $dsn is the Data Source Name for your database, for exmaple "mysql:dbname=my_oauth2_db;host=localhost"
-$storage = new OAuth2\Storage\Pdo($dbdata);
+$oauth2_storage = new OAuth2\Storage\Pdo($dbdata);
+
+// Set configuration
+$oauth2_config = array(
+  'require_exact_redirect_uri' => false,
+  'always_issue_new_refresh_token' => true, // Needs to be handed below as well as there it's not constructed from within the server object.
+  'refresh_token_lifetime' => 90*24*3600,
+);
 
 // Pass a storage object or array of storage objects to the OAuth2 server class
 
 // Pass a storage object or array of storage objects to the OAuth2 server class
-$server = new OAuth2\Server($storage);
+$server = new OAuth2\Server($oauth2_storage, $oauth2_config);
 
 // Add the "Client Credentials" grant type (it is the simplest of the grant types)
 
 // Add the "Client Credentials" grant type (it is the simplest of the grant types)
-$server->addGrantType(new OAuth2\GrantType\ClientCredentials($storage));
+//$server->addGrantType(new OAuth2\GrantType\ClientCredentials($storage));
 
 // Add the "Authorization Code" grant type (this is where the oauth magic happens)
 
 // Add the "Authorization Code" grant type (this is where the oauth magic happens)
-$server->addGrantType(new OAuth2\GrantType\AuthorizationCode($storage));
+$server->addGrantType(new OAuth2\GrantType\AuthorizationCode($oauth2_storage));
+
+// Add the "Refresh Token" grant type (required to get longer-living resource access by generating new access tokens)
+$server->addGrantType(new OAuth2\GrantType\RefreshToken($oauth2_storage, array('always_issue_new_refresh_token' => true)));
 
 ?>
 
 ?>