--- /dev/null
+ 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]
--- /dev/null
+<?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.')));
+}
+?>
// 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');
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
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;
}
// 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.
//
// 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.
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();
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;
$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']));
}
$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.'));
$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'));
}
+++ /dev/null
-<?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!'));
-
-?>
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)));
?>