From ea0452ad570f441f151a8a083e0810c12476a134 Mon Sep 17 00:00:00 2001 From: Robert Kaiser Date: Sat, 29 Oct 2016 18:29:39 +0200 Subject: [PATCH] create an API to retrieve emails and set new clients, add very rudimentary client management so master clients for the systems can be set, auto-authorize email scope, allow refresh tokens, give them 90 days validity --- .htaccess | 5 ++ api.php | 143 ++++++++++++++++++++++++++++++++++++++++++++ authorize.php | 4 +- authsystem.css | 16 +++++ authutils.php-class | 11 ++++ index.php | 76 +++++++++++++++++++++++ resource.php | 18 ------ server.inc.php | 18 ++++-- 8 files changed, 267 insertions(+), 24 deletions(-) create mode 100644 .htaccess create mode 100644 api.php delete mode 100644 resource.php diff --git a/.htaccess b/.htaccess new file mode 100644 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 index 0000000..b3096d1 --- /dev/null +++ b/api.php @@ -0,0 +1,143 @@ +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.'))); +} +?> diff --git a/authorize.php b/authorize.php index 4275254..33ffd09 100644 --- a/authorize.php +++ b/authorize.php @@ -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) -// 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'); @@ -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')); - $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 diff --git a/authsystem.css b/authsystem.css index a3f192f..a164fb9 100644 --- a/authsystem.css +++ b/authsystem.css @@ -28,6 +28,22 @@ form.flat { 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; } diff --git a/authutils.php-class b/authutils.php-class index 4d5680a..dc06a7f 100755 --- a/authutils.php-class +++ b/authutils.php-class @@ -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 $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. // @@ -46,6 +49,9 @@ class AuthUtils { // 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. @@ -82,6 +88,7 @@ class AuthUtils { 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(); @@ -166,6 +173,10 @@ class AuthUtils { 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; diff --git a/index.php b/index.php index ee03d8b..76e11d2 100644 --- 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.'); } } + 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 contact KaiRo.at 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'])); @@ -376,6 +409,45 @@ if (!count($errors)) { } $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.')); @@ -389,6 +461,10 @@ if (!count($errors)) { $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')); } diff --git a/resource.php b/resource.php deleted file mode 100644 index 436f415..0000000 --- a/resource.php +++ /dev/null @@ -1,18 +0,0 @@ -verifyResourceRequest(OAuth2\Request::createFromGlobals())) { - $server->getResponse()->send(); - die; -} -echo json_encode(array('success' => true, 'message' => 'You accessed my APIs!')); - -?> diff --git a/server.inc.php b/server.inc.php index 6a82b50..87b6535 100644 --- a/server.inc.php +++ b/server.inc.php @@ -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" -$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 -$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) -$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) -$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))); ?> -- 2.35.3