From 89346aaed9b3945cb91a3cc4f2af103d03e3f9a8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20=C3=85kre=20Solberg?= <andreas.solberg@uninett.no>
Date: Mon, 28 Sep 2009 09:50:24 +0000
Subject: [PATCH] OAuth module now uses permanent SQLlite storage instead of
 flat files...

git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@1792 44740490-163a-0410-bde0-09ae8108e29a
---
 modules/oauth/bin/demo.php                    |   3 +
 .../oauth/config-template/module_oauth.php    |  10 +-
 modules/oauth/hooks/hook_cron.php             |  30 ++++
 modules/oauth/hooks/hook_frontpage.php        |  18 +++
 modules/oauth/lib/Consumer.php                |   3 +-
 modules/oauth/lib/OAuthStore.php              |  87 +++++------
 modules/oauth/lib/Registry.php                | 144 ++++++++++++++++++
 modules/oauth/libextinc/OAuth.php             |  82 ++--------
 modules/oauth/templates/registry.edit.tpl.php |  21 +++
 modules/oauth/templates/registry.list.php     |  57 +++++++
 modules/oauth/templates/registry.saved.php    |  15 ++
 modules/oauth/www/accessToken.php             |   5 +
 modules/oauth/www/registry.edit.php           |  69 +++++++++
 modules/oauth/www/registry.php                |  58 +++++++
 modules/oauth/www/resources/style.css         |  37 +++++
 15 files changed, 514 insertions(+), 125 deletions(-)
 create mode 100644 modules/oauth/hooks/hook_cron.php
 create mode 100644 modules/oauth/hooks/hook_frontpage.php
 create mode 100644 modules/oauth/lib/Registry.php
 create mode 100644 modules/oauth/templates/registry.edit.tpl.php
 create mode 100644 modules/oauth/templates/registry.list.php
 create mode 100644 modules/oauth/templates/registry.saved.php
 create mode 100644 modules/oauth/www/registry.edit.php
 create mode 100644 modules/oauth/www/registry.php
 create mode 100644 modules/oauth/www/resources/style.css

diff --git a/modules/oauth/bin/demo.php b/modules/oauth/bin/demo.php
index 5391958e3..0c75e0e3c 100755
--- a/modules/oauth/bin/demo.php
+++ b/modules/oauth/bin/demo.php
@@ -16,6 +16,9 @@ require_once($baseDir . '/lib/_autoload.php');
 
 require_once(dirname(dirname(__FILE__)) . '/libextinc/OAuth.php');
 
+// Needed in order to make session_start to be called before output is printed.
+$session = SimpleSAML_Session::getInstance();
+
 $baseurl = (isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : 'https://foodle.feide.no/simplesaml');
 $key = (isset($_SERVER['argv'][2]) ? $_SERVER['argv'][2] : 'key');
 $secret = (isset($_SERVER['argv'][3]) ? $_SERVER['argv'][3] : 'secret');
diff --git a/modules/oauth/config-template/module_oauth.php b/modules/oauth/config-template/module_oauth.php
index a1a9c10b8..2d725d460 100644
--- a/modules/oauth/config-template/module_oauth.php
+++ b/modules/oauth/config-template/module_oauth.php
@@ -10,7 +10,15 @@ $config = array (
 	/* Enable the getUserInfo endpoint. Do not enable unless you know what you do.
 	 * It may give external parties access to userInfo unless properly secured.
 	 */
-	'getUserInfo.enable' => FALSE,
+	'getUserInfo.enable' => TRUE,
+	
+	'requestTokenDuration' => 60*30, // 30 minutes
+	'accessTokenDuration'  => 60*60*24, // 24 hours
+	'nonceCache'           => 60*60*24*14, // 14 days
+
+
+	// Tag to run storage cleanup script using the cron module...
+	'cron_tag' => 'hourly',
 
 );
 
diff --git a/modules/oauth/hooks/hook_cron.php b/modules/oauth/hooks/hook_cron.php
new file mode 100644
index 000000000..60483082b
--- /dev/null
+++ b/modules/oauth/hooks/hook_cron.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Hook to run a cron job.
+ *
+ * @param array &$croninfo  Output
+ */
+function oauth_hook_cron(&$croninfo) {
+	assert('is_array($croninfo)');
+	assert('array_key_exists("summary", $croninfo)');
+	assert('array_key_exists("tag", $croninfo)');
+
+	$oauthconfig = SimpleSAML_Configuration::getOptionalConfig('module_statistics.php');
+	
+	if (is_null($oauthconfig->getValue('cron_tag', 'hourly'))) return;
+	if ($oauthconfig->getValue('cron_tag', NULL) !== $croninfo['tag']) return;
+	
+	try {
+		$store = new sspmod_core_Storage_SQLPermanentStorage('oauth');
+		$cleaned = $store->removeExpired();
+		
+#		if ($cleaned > 0) 
+			$croninfo['summary'][] = 'OAuth clean up. Removed ' . $cleaned . ' expired entries from OAuth storage.';
+		
+	} catch (Exception $e) {
+		$message = 'OAuth clean up cron script failed: ' . $e->getMessage();
+		SimpleSAML_Logger::warning($message);
+		$croninfo['summary'][] = $message;
+	}
+}
+?>
\ No newline at end of file
diff --git a/modules/oauth/hooks/hook_frontpage.php b/modules/oauth/hooks/hook_frontpage.php
new file mode 100644
index 000000000..a9733b1af
--- /dev/null
+++ b/modules/oauth/hooks/hook_frontpage.php
@@ -0,0 +1,18 @@
+<?php
+/**
+ * Hook to add link to the frontpage.
+ *
+ * @param array &$links  The links on the frontpage, split into sections.
+ */
+function oauth_hook_frontpage(&$links) {
+	assert('is_array($links)');
+	assert('array_key_exists("links", $links)');
+
+	$links['federation']['oauthregistry'] = array(
+		'href' => SimpleSAML_Module::getModuleURL('oauth/registry.php'),
+		'text' => array('en' => 'OAuth Consumer Registry'),
+		'shorttext' => array('en' => 'OAuth Registry'),
+	);
+
+}
+?>
\ No newline at end of file
diff --git a/modules/oauth/lib/Consumer.php b/modules/oauth/lib/Consumer.php
index 615bd3fac..764856b8e 100644
--- a/modules/oauth/lib/Consumer.php
+++ b/modules/oauth/lib/Consumer.php
@@ -35,7 +35,7 @@ class sspmod_oauth_Consumer {
 		
 		if(array_key_exists('error', $responseParsed))
 			throw new Exception('Error getting request token: ') . $responseParsed['error'];
-		
+			
 #		echo('<pre>'); print_r($response_req); exit;
 		
 		$requestToken = $responseParsed['oauth_token'];
@@ -66,6 +66,7 @@ class sspmod_oauth_Consumer {
 			throw new Exception('Error contacting request_token endpoint on the OAuth Provider');
 		}
 
+		SimpleSAML_Logger::info('   ==== RESPONSE: '. $response_acc);
 		
 		parse_str($response_acc, $accessResponseParsed);
 		
diff --git a/modules/oauth/lib/OAuthStore.php b/modules/oauth/lib/OAuthStore.php
index 765b93a83..2471e1014 100644
--- a/modules/oauth/lib/OAuthStore.php
+++ b/modules/oauth/lib/OAuthStore.php
@@ -11,90 +11,71 @@ require_once(dirname(dirname(__FILE__)) . '/libextinc/OAuth.php');
  */
 class sspmod_oauth_OAuthStore extends OAuthDataStore {
 
-	private $path;
+	private $store;
+	private $config;
 
-    function __construct($path = '/tmp/oauth/') {
-			
-		if (!file_exists($path)) {
-			mkdir($path);
-		}
-		if (!is_dir($path)) 
-			throw new Exception('OAuth Storage Path [' . $path . '] is not a valid directory');
-			
-		$this->path = $path;
+    function __construct() {
+		$this->store = new sspmod_core_Storage_SQLPermanentStorage('oauth');			
+		$this->config = SimpleSAML_Configuration::getOptionalConfig('module_oauth.php');
     }
-
-	private function filename($key) {
-		return $this->path . sha1($key) . '.oauthstore';
-	}
-	
-	private function exists($key) {
-		return file_exists($this->filename($key));
-	}
-	
-	private function get($key) {
-		error_log( 'Getting :' . $key . ' : ' . ($this->exists($key) ? 'FOUND' : 'NOTFOUND'));
-		if (!$this->exists($key)) return NULL;
-		return unserialize(file_get_contents($this->filename($key)));
-	}
-	
-	private function set($key, $value) {
-		error_log('Setting :' . $key . ' : ' . ($this->exists($key) ? 'FOUND' : 'NOTFOUND'));
-		file_put_contents($this->filename($key), serialize($value));
-	}
-
-	private function remove($key) {
-		unlink($this->filename($key));
-	}
 	
 	public function authorize($requestToken, $data) {
-		$this->set('validated.request.' . $requestToken, $data);
+		# set($type, $key1, $key2, $value, $duration = NULL) {
+		$this->store->set('authorized', $requestToken, '', $data, $this->config->getValue('requestTokenDuration', 60*30) );
 	}
 	
 	public function isAuthorized($requestToken) {
-		return $this->exists('validated.request.' . $requestToken);
+		SimpleSAML_Logger::info('OAuth isAuthorized(' . $requestToken . ')');
+		return $this->store->exists('authorized', $requestToken, '');
 	}
 	
 	public function getAuthorizedData($token) {
-		return $this->get('validated.request.' . $token);
+		SimpleSAML_Logger::info('OAuth getAuthorizedData(' . $token . ')');
+		$data = $this->store->get('authorized', $token, '');
+		return $data['value'];
 	}
 	
 	public function moveAuthorizedData($requestToken, $accessToken) {
+		SimpleSAML_Logger::info('OAuth moveAuthorizedData(' . $requestToken . ', ' . $accessToken . ')');
 		$this->authorize($accessToken, $this->getAuthorizedData($requestToken));
-		$this->remove('validated.request.' . $requestToken);
+		$this->store->remove('authorized', $requestToken, '');
 	}
 	
-	private function tokenTag($tokenType = 'default', $token) {
-		return 'token.' . $token . '.' . $tokenType;
-	}
-
-    function lookup_consumer($consumer_key) {
-        if ($consumer_key == 'key') return new OAuthConsumer("key", "secret", NULL);
-        return NULL;
+    public function lookup_consumer($consumer_key) {
+		
+		SimpleSAML_Logger::info('OAuth lookup_consumer(' . $consumer_key . ')');
+		if (! $this->store->exists('consumers', $consumer_key, ''))  return NULL;
+		$consumer = $this->store->get('consumers', $consumer_key, '');
+		// SimpleSAML_Logger::info('OAuth consumer dump(' . var_export($consumer, TRUE) . ')');
+		return new OAuthConsumer($consumer['value']['key'], $consumer['value']['secret'], NULL);
     }
 
     function lookup_token($consumer, $tokenType = 'default', $token) {
-		return $this->get($this->tokenTag($tokenType, $token));
+		SimpleSAML_Logger::info('OAuth lookup_token(' . $consumer->key . ', ' . $tokenType. ',' . $token . ')');
+		$data = $this->store->get($tokenType, $token, $consumer->key);
+		if ($data == NULL) throw new Exception('Could not find token');
+		return $data['value'];
     }
 
     function lookup_nonce($consumer, $token, $nonce, $timestamp) {
-		$nonceTag = 'nonce.' . $consumer->key . '.' . $nonce;
-		if ($this->exists($nonceTag))
-			return TRUE;
-		
-		$this->set($nonceTag, $timestamp);
+		SimpleSAML_Logger::info('OAuth lookup_nonce(' . $consumer . ', ' . $token. ',' . $nonce . ')');
+		if ($this->store->exists('nonce', $nonce, $consumer->key))  return TRUE;
+		$this->store->set('nonce', $nonce, $consumer->key, TRUE, $this->config->getValue('nonceCache', 60*60*24*14));
 		return FALSE;
     }
 
     function new_request_token($consumer) {
+		SimpleSAML_Logger::info('OAuth new_request_token(' . $consumer . ')');
 		$token = new OAuthToken(SimpleSAML_Utilities::generateID(), SimpleSAML_Utilities::generateID());
-		$this->set($this->tokenTag('request', $token->key), $token);
+		$this->store->set('request', $token->key, $consumer->key, $token, $this->config->getValue('requestTokenDuration', 60*30) );
         return $token;
     }
 
-    function new_access_token($token, $consumer) {
+    function new_access_token($requestToken, $consumer) {
+		SimpleSAML_Logger::info('OAuth new_access_token(' . $requestToken . ',' . $consumer . ')');
 		$token = new OAuthToken(SimpleSAML_Utilities::generateID(), SimpleSAML_Utilities::generateID());
-		$this->set($this->tokenTag('access', $token->key), $token);
+		// SimpleSAML_Logger::info('OAuth new_access_token(' . $requestToken . ',' . $consumer . ',' . $token . ')');
+		$this->store->set('access', $token->key, $consumer->key, $token, $this->config->getValue('accessTokenDuration', 60*60*24) );
         return $token;
     }
 
diff --git a/modules/oauth/lib/Registry.php b/modules/oauth/lib/Registry.php
new file mode 100644
index 000000000..06abcd426
--- /dev/null
+++ b/modules/oauth/lib/Registry.php
@@ -0,0 +1,144 @@
+<?php
+
+/**
+ * Editor for OAuth Client Registry
+ *
+ * @author Andreas Ă…kre Solberg <andreas@uninett.no>, UNINETT AS.
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+class sspmod_oauth_Registry {
+
+
+	protected function getStandardField($request, &$entry, $key) {
+		if (array_key_exists('field_' . $key, $request)) {
+			$entry[$key] = $request['field_' . $key];
+		} else {
+			if (isset($entry[$key])) unset($entry[$key]);
+		}
+	}
+
+	public function formToMeta($request, $entry = array(), $override = NULL) {
+		$this->getStandardField($request, $entry, 'name');
+		$this->getStandardField($request, $entry, 'description');
+		$this->getStandardField($request, $entry, 'key');
+		$this->getStandardField($request, $entry, 'secret');
+		
+		if ($override) {
+			foreach($override AS $key => $value) {
+				$entry[$key] = $value;
+			}
+		}
+		
+		return $entry;
+	}
+
+	protected function requireStandardField($request, $key) {
+		if (!array_key_exists('field_' . $key, $request))
+			throw new Exception('Required field [' . $key . '] was missing.');
+		if (empty($request['field_' . $key]))
+			throw new Exception('Required field [' . $key . '] was empty.');
+	}
+
+	public function checkForm($request) {
+		$this->requireStandardField($request, 'name');
+		$this->requireStandardField($request, 'description');
+	}
+	
+
+	protected function header($name) {
+		return '<tr ><td>&nbsp;</td><td class="header">' . $name . '</td></tr>';
+		
+	}
+	
+	protected function readonlyDateField($metadata, $key, $name) {
+		$value = '<span style="color: #aaa">Not set</a>';
+		if (array_key_exists($key, $metadata))
+			$value = date('j. F Y, G:i', $metadata[$key]);
+		return '<tr>
+			<td class="name">' . $name . '</td>
+			<td class="data">' . $value . '</td></tr>';
+
+	}
+	
+	protected function readonlyField($metadata, $key, $name) {
+		$value = '';
+		if (array_key_exists($key, $metadata))
+			$value = $metadata[$key];
+		return '<tr>
+			<td class="name">' . $name . '</td>
+			<td class="data">' . htmlspecialchars($value) . '</td></tr>';
+
+	}
+	
+	protected function hiddenField($key, $value) {
+		return '<input type="hidden" name="' . $key . '" value="' . htmlspecialchars($value) . '" />';
+	}
+	
+	protected function flattenLanguageField(&$metadata, $key) {
+		if (array_key_exists($key, $metadata)) {
+			if (is_array($metadata[$key])) {
+				if (isset($metadata[$key]['en'])) {
+					$metadata[$key] = $metadata[$key]['en'];
+				} else {
+					unset($metadata[$key]);
+				}
+			}
+		}
+	}
+	
+	protected function standardField($metadata, $key, $name, $textarea = FALSE) {
+		$value = '';
+		if (array_key_exists($key, $metadata)) {
+			$value = htmlspecialchars($metadata[$key]);
+		}
+		
+		if ($textarea) {
+			return '<tr><td class="name">' . $name . '</td><td class="data">
+			<textarea name="field_' . $key . '" rows="5" cols="50">' . $value . '</textarea></td></tr>';
+			
+		} else {
+			return '<tr><td class="name">' . $name . '</td><td class="data">
+			<input type="text" size="60" name="field_' . $key . '" value="' . $value . '" /></td></tr>';
+			
+		}
+	}
+
+	public function metaToForm($metadata) {
+		// $this->flattenLanguageField($metadata, 'name');
+		// $this->flattenLanguageField($metadata, 'description');
+		
+		return '<form action="registry.edit.php" method="post">' .		
+			'<div id="tabdiv">' .
+			'<ul>' .
+			'<li><a href="#basic">Name and descrition</a></li>' . 
+			// '<li><a href="#saml">SAML 2.0</a></li>' . 
+			// '<li><a href="#attributes">Attributes</a></li>' . 
+			// '<li><a href="#orgs">Organizations</a></li>' . 
+			// '<li><a href="#contacts">Contacts</a></li>' . 
+			'</ul>' .
+			'<div id="basic"><table class="formtable">' .
+				$this->standardField($metadata, 'name', 'Name of client') .
+				$this->standardField($metadata, 'description', 'Description of client', TRUE) .
+				$this->readonlyField($metadata, 'owner', 'Owner') .
+				$this->readonlyField($metadata, 'key', 'Consumer Key') .
+				$this->readonlyField($metadata, 'secret', 'Consumer Secret') .
+				
+				$this->hiddenField('field_key', $metadata['key']) .
+				$this->hiddenField('field_secret', $metadata['secret']) .
+
+			'</table></div>' . 
+			// '<div id="saml"><table class="formtable">' .
+			// 	$this->standardField($metadata, 'AssertionConsumerService', 'AssertionConsumerService endpoint') .
+			// 	$this->standardField($metadata, 'SingleLogoutService', 'SingleLogoutService endpoint') .
+			// 	// $this->standardField($metadata, 'certFingerprint', 'Certificate Fingerprint') .			
+			// 	
+			// '</table></div>' .
+			'</div>' .
+			'<input type="submit" name="submit" value="Save" style="margin-top: 5px" />' .
+		'</form>';
+	}
+	
+}
+
+
diff --git a/modules/oauth/libextinc/OAuth.php b/modules/oauth/libextinc/OAuth.php
index 029166175..cb26ed767 100644
--- a/modules/oauth/libextinc/OAuth.php
+++ b/modules/oauth/libextinc/OAuth.php
@@ -206,7 +206,7 @@ class OAuthRequest {/*{{{*/
       // next check for the auth header, we need to do some extra stuff
       // if that is the case, namely suck in the parameters from GET or POST
       // so that we can include them in the signature
-      if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") {
+      if (array_key_exists('Authorization', $request_headers) &&  substr($request_headers['Authorization'], 0, 6) == "OAuth ") {
         $header_parameters = OAuthRequest::split_header($request_headers['Authorization']);
         $parameters = array_merge($req_parameters, $header_parameters);
         $req = new OAuthRequest($http_method, $http_url, $parameters);
@@ -589,6 +589,16 @@ class OAuthServer {/*{{{*/
    */
   private function get_token(&$request, $consumer, $token_type="access") {/*{{{*/
     $token_field = @$request->get_parameter('oauth_token');
+
+	// SimpleSAML_Logger::info('request: ' . var_export($request, TRUE));
+	// SimpleSAML_Logger::info('token_type: ' . var_export($token_type, TRUE));
+	// SimpleSAML_Logger::info('token_field: ' . var_export($token_field, TRUE));
+	// 
+	// $bt = SimpleSAML_Utilities::buildBacktrace(new Exception());
+	// foreach ($bt AS $t) {
+	// 	SimpleSAML_Logger::info('   ' . $t);
+	// }
+
     $token = $this->data_store->lookup_token(
       $consumer, $token_type, $token_field
     );
@@ -612,7 +622,7 @@ class OAuthServer {/*{{{*/
 
     $signature_method = $this->get_signature_method($request);
 
-    $signature = $request->get_parameter('oauth_signature');    
+    $signature = $request->get_parameter('oauth_signature'); 
     $valid_sig = $signature_method->check_signature(
       $request, 
       $consumer, 
@@ -678,74 +688,6 @@ class OAuthDataStore {/*{{{*/
 }/*}}}*/
 
 
-/*  A very naive dbm-based oauth storage
- */
-class SimpleOAuthDataStore extends OAuthDataStore {/*{{{*/
-  private $dbh;
-
-  function __construct($path = "oauth.gdbm") {/*{{{*/
-    $this->dbh = dba_popen($path, 'c', 'gdbm');
-  }/*}}}*/
-
-  function __destruct() {/*{{{*/
-    dba_close($this->dbh);
-  }/*}}}*/
-
-  function lookup_consumer($consumer_key) {/*{{{*/
-    $rv = dba_fetch("consumer_$consumer_key", $this->dbh);
-    if ($rv === FALSE) {
-      return NULL;
-    }
-    $obj = unserialize($rv);
-    if (!($obj instanceof OAuthConsumer)) {
-      return NULL;
-    }
-    return $obj;
-  }/*}}}*/
-
-  function lookup_token($consumer, $token_type, $token) {/*{{{*/
-    $rv = dba_fetch("${token_type}_${token}", $this->dbh);
-    if ($rv === FALSE) {
-      return NULL;
-    }
-    $obj = unserialize($rv);
-    if (!($obj instanceof OAuthToken)) {
-      return NULL;
-    }
-    return $obj;
-  }/*}}}*/
-
-  function lookup_nonce($consumer, $token, $nonce, $timestamp) {/*{{{*/
-    if (dba_exists("nonce_$nonce", $this->dbh)) {
-      return TRUE;
-    } else {
-      dba_insert("nonce_$nonce", "1", $this->dbh);
-      return FALSE;
-    }
-  }/*}}}*/
-
-  function new_token($consumer, $type="request") {/*{{{*/
-    $key = md5(time());
-    $secret = time() + time();
-    $token = new OAuthToken($key, md5(md5($secret)));
-    if (!dba_insert("${type}_$key", serialize($token), $this->dbh)) {
-      throw new OAuthException("doooom!");
-    }
-    return $token;
-  }/*}}}*/
-
-  function new_request_token($consumer) {/*{{{*/
-    return $this->new_token($consumer, "request");
-  }/*}}}*/
-
-  function new_access_token($token, $consumer) {/*{{{*/
-
-    $token = $this->new_token($consumer, 'access');
-    dba_delete("request_" . $token->key, $this->dbh);
-    return $token;
-  }/*}}}*/
-}/*}}}*/
-
 class OAuthUtil {/*{{{*/
   public static function urlencode_rfc3986($input) {/*{{{*/
 	if (is_array($input)) {
diff --git a/modules/oauth/templates/registry.edit.tpl.php b/modules/oauth/templates/registry.edit.tpl.php
new file mode 100644
index 000000000..d0841a106
--- /dev/null
+++ b/modules/oauth/templates/registry.edit.tpl.php
@@ -0,0 +1,21 @@
+<?php
+
+$this->data['jquery'] = array('version' => '1.6', 'core' => TRUE, 'ui' => TRUE, 'css' => TRUE);
+$this->data['head']  = '<link rel="stylesheet" type="text/css" href="/' . $this->data['baseurlpath'] . 'module.php/metaedit/resources/style.css" />' . "\n";
+$this->data['head'] .= '<script type="text/javascript">
+$(document).ready(function() {
+	$("#tabdiv").tabs();
+});
+</script>';
+
+$this->includeAtTemplateBase('includes/header.php');
+
+
+echo('<h1>OAuth Client</h1>');
+
+echo($this->data['form']);
+
+echo('<p style="float: right"><a href="registry.php">Return to entity listing <strong>without saving...</strong></a></p>');
+
+$this->includeAtTemplateBase('includes/footer.php');
+
diff --git a/modules/oauth/templates/registry.list.php b/modules/oauth/templates/registry.list.php
new file mode 100644
index 000000000..1a11da862
--- /dev/null
+++ b/modules/oauth/templates/registry.list.php
@@ -0,0 +1,57 @@
+<?php
+
+$this->data['jquery'] = array('version' => '1.6', 'core' => TRUE, 'ui' => TRUE, 'css' => TRUE);
+$this->data['head']  = '<link rel="stylesheet" type="text/css" href="/' . $this->data['baseurlpath'] . 'module.php/oauth/resources/style.css" />' . "\n";
+// $this->data['head'] .= '<script type="text/javascript">
+// $(document).ready(function() {
+// 	$("#tabdiv").tabs();
+// });
+// </script>';
+
+$this->includeAtTemplateBase('includes/header.php');
+
+
+echo('<h1>OAuth Client Registry</h1>');
+
+echo('<p>Here you can register new OAuth Clients. You are successfully logged in as ' . $this->data['userid'] . '</p>');
+
+echo('<h2>Your clients</h2>');
+echo('<table class="metalist" style="width: 100%">');
+$i = 0; $rows = array('odd', 'even');
+foreach($this->data['entries']['mine'] AS $entryc ) {
+	$entry = $entryc['value'];
+	$i++; 
+	echo('<tr class="' . $rows[$i % 2] . '">
+		<td>' . $entry['name'] . '</td>
+		<td><tt>' . $entry['key'] . '</tt></td>
+		<td>
+			<a href="registry.edit.php?editkey=' . urlencode($entry['key']) . '">edit</a>
+			<a href="registry.php?delete=' . urlencode($entry['key']) . '">delete</a>
+		</td></tr>');
+}
+if ($i == 0) {
+	echo('<tr><td colspan="3">No entries registered</td></tr>');
+}
+echo('</table>');
+
+echo('<p><a href="registry.edit.php">Add new client</a></p>');
+
+echo('<h2>Other clients</h2>');
+echo('<table class="metalist" style="width: 100%">');
+$i = 0; $rows = array('odd', 'even');
+foreach($this->data['entries']['others'] AS $entryc ) {
+	$entry = $entryc['value'];
+	$i++; 
+	echo('<tr class="' . $rows[$i % 2] . '">
+		<td>' . $entry['name'] . '</td>
+		<td><tt>' . $entry['key'] . '</tt></td>
+		<td>' . (isset($entry['owner']) ? $entry['owner'] : 'No owner') . '
+		</td></tr>');
+}
+if ($i == 0) {
+	echo('<tr><td colspan="3">No entries registered</td></tr>');
+}
+echo('</table>');
+
+$this->includeAtTemplateBase('includes/footer.php');
+
diff --git a/modules/oauth/templates/registry.saved.php b/modules/oauth/templates/registry.saved.php
new file mode 100644
index 000000000..0ff62eb50
--- /dev/null
+++ b/modules/oauth/templates/registry.saved.php
@@ -0,0 +1,15 @@
+<?php
+
+
+
+$this->includeAtTemplateBase('includes/header.php');
+
+
+echo('<h1>OAuth Client saved</h1>');
+
+echo('<p><a href="registry.php">Go back to OAuth client listing</a></p>');
+
+
+
+$this->includeAtTemplateBase('includes/footer.php');
+
diff --git a/modules/oauth/www/accessToken.php b/modules/oauth/www/accessToken.php
index d4de1f7f1..598b91617 100644
--- a/modules/oauth/www/accessToken.php
+++ b/modules/oauth/www/accessToken.php
@@ -12,13 +12,18 @@ $server->add_signature_method($hmac_method);
 $server->add_signature_method($plaintext_method);
 
 $req = OAuthRequest::from_request();
+
+
 $requestToken = $req->get_parameter('oauth_token');
 
 
+
 if (!$store->isAuthorized($requestToken)) {
 	throw new Exception('Your request was not authorized. Request token [' . $requestToken . '] not found.');
 }
 
+
+
 $accessToken = $server->fetch_access_token($req);
 
 $data = $store->moveAuthorizedData($requestToken, $accessToken->key);
diff --git a/modules/oauth/www/registry.edit.php b/modules/oauth/www/registry.edit.php
new file mode 100644
index 000000000..b0283198d
--- /dev/null
+++ b/modules/oauth/www/registry.edit.php
@@ -0,0 +1,69 @@
+<?php
+
+/* Load simpleSAMLphp, configuration and metadata */
+$config = SimpleSAML_Configuration::getInstance();
+$session = SimpleSAML_Session::getInstance();
+$oauthconfig = SimpleSAML_Configuration::getOptionalConfig('module_oauth.php');
+
+$store = new sspmod_core_Storage_SQLPermanentStorage('oauth');
+
+$authsource = $oauthconfig->getValue('auth', 'admin');
+$useridattr = $oauthconfig->getValue('useridattr', 'user');
+
+if ($session->isValid($authsource)) {
+	$attributes = $session->getAttributes();
+	// Check if userid exists
+	if (!isset($attributes[$useridattr])) 
+		throw new Exception('User ID is missing');
+	$userid = $attributes[$useridattr][0];
+} else {
+	SimpleSAML_Auth_Default::initLogin($authsource, SimpleSAML_Utilities::selfURL());
+}
+
+function requireOwnership($entry, $userid) {
+	if (!isset($entry['owner']))
+		throw new Exception('OAuth Consumer has no owner. Which means no one is granted access, not even you.');
+	if ($entry['owner'] !== $userid) 
+		throw new Exception('OAuth Consumer has an owner that is not equal to your userid, hence you are not granted access.');
+}
+
+if (array_key_exists('editkey', $_REQUEST)) {
+	$entryc = $store->get('consumers', $_REQUEST['editkey'], '');
+	$entry = $entryc['value'];
+	requireOwnership($entry, $userid);
+	
+} else {
+	$entry = array(
+		'owner' => $userid,
+		'key' => SimpleSAML_Utilities::generateID(),
+		'secret' => SimpleSAML_Utilities::generateID(),
+	);
+}
+
+
+$editor = new sspmod_oauth_Registry();
+
+
+if (isset($_POST['submit'])) {
+	$editor->checkForm($_POST);
+
+	$entry = $editor->formToMeta($_POST, array(), array('owner' => $userid));
+
+	requireOwnership($entry, $userid);
+	
+#	echo('<pre>Created: '); print_r($entry); exit;
+	
+	$store->set('consumers', $entry['key'], '', $entry);
+	
+	$template = new SimpleSAML_XHTML_Template($config, 'oauth:registry.saved.php');
+	$template->data['entry'] = $entry;
+	$template->show();
+	exit;
+}
+
+$form = $editor->metaToForm($entry);
+
+$template = new SimpleSAML_XHTML_Template($config, 'oauth:registry.edit.tpl.php');
+$template->data['form'] = $form;
+$template->show();
+
diff --git a/modules/oauth/www/registry.php b/modules/oauth/www/registry.php
new file mode 100644
index 000000000..51bd0e8a2
--- /dev/null
+++ b/modules/oauth/www/registry.php
@@ -0,0 +1,58 @@
+<?php
+
+/* Load simpleSAMLphp, configuration and metadata */
+$config = SimpleSAML_Configuration::getInstance();
+$session = SimpleSAML_Session::getInstance();
+$oauthconfig = SimpleSAML_Configuration::getOptionalConfig('module_oauth.php');
+
+$store = new sspmod_core_Storage_SQLPermanentStorage('oauth');
+
+$authsource = $oauthconfig->getValue('auth', 'admin');
+$useridattr = $oauthconfig->getValue('useridattr', 'user');
+
+if ($session->isValid($authsource)) {
+	$attributes = $session->getAttributes();
+	// Check if userid exists
+	if (!isset($attributes[$useridattr])) 
+		throw new Exception('User ID is missing');
+	$userid = $attributes[$useridattr][0];
+} else {
+	SimpleSAML_Auth_Default::initLogin($authsource, SimpleSAML_Utilities::selfURL());
+}
+
+function requireOwnership($entry, $userid) {
+	if (!isset($entry['owner']))
+		throw new Exception('OAuth Consumer has no owner. Which means no one is granted access, not even you.');
+	if ($entry['owner'] !== $userid) 
+		throw new Exception('OAuth Consumer has an owner that is not equal to your userid, hence you are not granted access.');
+}
+
+
+if (isset($_REQUEST['delete'])) {
+	$entryc = $store->get('consumers', $_REQUEST['delete'], '');
+	$entry = $entryc['value'];
+
+	requireOwnership($entry, $userid);
+	$store->remove('consumers', $entry['key'], '');
+}
+
+
+$list = $store->getList('consumers');
+
+$slist = array('mine' => array(), 'others' => array());
+if (is_array($list)) 
+foreach($list AS $listitem) {
+	if (array_key_exists('owner', $listitem['value'])) {
+		if ($listitem['value']['owner'] === $userid) {
+			$slist['mine'][] = $listitem; continue;
+		}
+	}
+	$slist['others'][] = $listitem;
+}
+
+// echo('<pre>'); print_r($slist); exit;
+
+$template = new SimpleSAML_XHTML_Template($config, 'oauth:registry.list.php');
+$template->data['entries'] = $slist;
+$template->data['userid'] = $userid;
+$template->show();
diff --git a/modules/oauth/www/resources/style.css b/modules/oauth/www/resources/style.css
new file mode 100644
index 000000000..1240db065
--- /dev/null
+++ b/modules/oauth/www/resources/style.css
@@ -0,0 +1,37 @@
+table.formtable {
+	width: 100%;
+}
+table.formtable tr td.name {
+	text-align: right;
+	vertical-align: top;
+	padding-right: .6em;
+}
+table.formtable tr td.value {
+	text-align: left;
+	padding: 0px;
+}
+table.formtable tr td.header {
+	padding-left: 5px;
+	padding-top: 8px;
+	font-weight: bold;
+	font-size: 110%;
+}
+
+table.formtable tr td input,table.formtable tr td textarea {
+	width: 90%;
+	border: 1px solid #bbb;
+	margin: 2px 5px;
+	padding: 2px 4px;
+}
+
+
+table.metalist {
+	border: 1px solid #aaa;
+	border-collapse: collapse;
+}
+table.metalist tr td {
+	padding: 2px 5px; 
+}
+table.metalist tr.even td {
+	background: #e5e5e5;
+}
\ No newline at end of file
-- 
GitLab