From 725838cc2ccd385c762a9325c160ede86352f2d9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20=C3=85kre=20Solberg?= <andreas.solberg@uninett.no>
Date: Wed, 24 Sep 2008 13:17:17 +0000
Subject: [PATCH] Adding OpenID Consumer Authentiation Module

git-svn-id: https://simplesamlphp.googlecode.com/svn/trunk@890 44740490-163a-0410-bde0-09ae8108e29a
---
 modules/openid/default-disable                |   3 +
 modules/openid/dictionaries/dictopenid.php    |  16 ++
 modules/openid/hooks/hook_frontpage.php       |  17 ++
 .../openid/lib/Auth/Source/OpenIDConsumer.php |  45 ++++
 modules/openid/templates/default/consumer.php |  67 ++++++
 modules/openid/www/consumer.php               | 213 ++++++++++++++++++
 modules/openid/www/openidtest.php             |  32 +++
 www/resources/icons/openid.png                | Bin 0 -> 3870 bytes
 8 files changed, 393 insertions(+)
 create mode 100644 modules/openid/default-disable
 create mode 100644 modules/openid/dictionaries/dictopenid.php
 create mode 100644 modules/openid/hooks/hook_frontpage.php
 create mode 100644 modules/openid/lib/Auth/Source/OpenIDConsumer.php
 create mode 100644 modules/openid/templates/default/consumer.php
 create mode 100644 modules/openid/www/consumer.php
 create mode 100644 modules/openid/www/openidtest.php
 create mode 100644 www/resources/icons/openid.png

diff --git a/modules/openid/default-disable b/modules/openid/default-disable
new file mode 100644
index 000000000..fa0bd82e2
--- /dev/null
+++ b/modules/openid/default-disable
@@ -0,0 +1,3 @@
+This file indicates that the default state of this module
+is disabled. To enable, create a file named enable in the
+same directory as this file.
diff --git a/modules/openid/dictionaries/dictopenid.php b/modules/openid/dictionaries/dictopenid.php
new file mode 100644
index 000000000..a4e70a364
--- /dev/null
+++ b/modules/openid/dictionaries/dictopenid.php
@@ -0,0 +1,16 @@
+<?php 
+ 
+$lang = array( 
+
+	'openidtestpage' => array (
+		'en' => 'OpenID Consumer Authentication Test',
+		'no' => 'OpenID Consumer Autentiserings Test',
+	),
+ 
+); 
+ 
+ 
+?> 
+ 
+ 
+ 
diff --git a/modules/openid/hooks/hook_frontpage.php b/modules/openid/hooks/hook_frontpage.php
new file mode 100644
index 000000000..aec1e20c3
--- /dev/null
+++ b/modules/openid/hooks/hook_frontpage.php
@@ -0,0 +1,17 @@
+<?php
+/**
+ * Hook to add the modinfo module to the frontpage.
+ *
+ * @param array &$links  The links on the frontpage, split into sections.
+ */
+function openid_hook_frontpage(&$links) {
+	assert('is_array($links)');
+	assert('array_key_exists("links", $links)');
+
+	$links['links'][] = array(
+		'href' => SimpleSAML_Module::getModuleURL('openid/openidtest.php'),
+		'text' => '{openid:dictopenid:openidtestpage}',
+	);
+
+}
+?>
\ No newline at end of file
diff --git a/modules/openid/lib/Auth/Source/OpenIDConsumer.php b/modules/openid/lib/Auth/Source/OpenIDConsumer.php
new file mode 100644
index 000000000..0d42f83e0
--- /dev/null
+++ b/modules/openid/lib/Auth/Source/OpenIDConsumer.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * Authentication module which acts as an OpenID Consumer
+ *
+ * @author Andreas Ă…kre Solberg, <andreas.solberg@uninett.no>, UNINETT AS.
+ * @package simpleSAMLphp
+ * @version $Id$
+ */
+class sspmod_openid_Auth_Source_OpenIDConsumer extends SimpleSAML_Auth_Source {
+
+
+	/**
+	 * Constructor for this authentication source.
+	 *
+	 * @param array $info  Information about this authentication source.
+	 * @param array $config  Configuration.
+	 */
+	public function __construct($info, $config) {
+
+		/* Call the parent constructor first, as required by the interface. */
+		parent::__construct($info, $config);
+
+	}
+
+
+	/**
+	 * Initiate authentication. Redirecting the user to the consumer endpoint 
+	 * with a state Auth ID.
+	 *
+	 * @param array &$state  Information about the current authentication.
+	 */
+	public function authenticate(&$state) {
+		assert('is_array($state)');
+
+		$state['openid:AuthId'] = $this->authId;
+		$id = SimpleSAML_Auth_State::saveState($state, 'openid:state');
+
+		$url = SimpleSAML_Module::getModuleURL('openid/consumer.php');
+		SimpleSAML_Utilities::redirect($url, array('AuthState' => $id));
+	}
+
+}
+
+?>
\ No newline at end of file
diff --git a/modules/openid/templates/default/consumer.php b/modules/openid/templates/default/consumer.php
new file mode 100644
index 000000000..e056d045a
--- /dev/null
+++ b/modules/openid/templates/default/consumer.php
@@ -0,0 +1,67 @@
+<?php
+
+$this->data['header'] = 'OpenID Login';
+$this->data['icon'] = 'openid.png';
+$this->data['autofocus'] = 'openid-identifier';
+$this->includeAtTemplateBase('includes/header.php');
+
+?>
+<style>
+input.openid-identifier {
+   background: url(http://stat.livejournal.com/img/openid-inputicon.gif) no-repeat;
+/*   background-color: #fff; */
+	border-left: 1px solid #ccc;
+	border-right: 1px solid #aaa;
+	border-top: 1px solid #aaa;
+	border-bottom: 1px solid #ccc;
+	color: #555;
+   background-position: 0 50%;
+   padding-left: 18px;
+}
+fieldset {
+	border-left: 1px solid #aaa;
+	border-right: 1px solid #ccc;
+	border-top: 1px solid #ccc;
+	border-bottom: 1px solid #aaa;
+	padding: 1em;
+}
+legend {
+	padding-left: .3em;
+	padding-right: .3em;
+	color: #555;
+}
+
+div.error {
+	padding: 1em; margin: 1em;
+	background: red;
+	color: white;
+	border: 1px solid #600;
+}
+</style>
+<div id="content">
+
+
+    <?php if (isset($this->data['error'])) { print "<div class=\"error\">" . $this->data['error'] . "</div>"; } ?>
+
+
+
+	<form method="get" action="consumer.php">
+		<fieldset>
+			<legend>OpenID Login</legend>
+
+			Identity&nbsp;URL:
+			<input type="hidden" name="action" value="verify" />
+			<input id="openid-identifier" class="openid-identifier" type="text" name="openid_url" value="http://" />
+			<input type="hidden" name="AuthState" value="<?php echo $this->data['AuthState']; ?>" />
+			<input type="submit" value="Login with OpenID" />
+		</fieldset>
+	</form>
+
+    <p style="margin-top: 2em">
+       OpenID is a free and easy way to use a single digital identity across the Internet. Enter your OpenID identity URL in the box above to authenticate.
+    </p>
+
+
+<?php
+$this->includeAtTemplateBase('includes/footer.php');
+?>
\ No newline at end of file
diff --git a/modules/openid/www/consumer.php b/modules/openid/www/consumer.php
new file mode 100644
index 000000000..159b30723
--- /dev/null
+++ b/modules/openid/www/consumer.php
@@ -0,0 +1,213 @@
+<?php
+
+#require_once('../../_include.php');
+require_once('Auth/OpenID/SReg.php');
+require_once('Auth/OpenID/Server.php');
+require_once('Auth/OpenID/ServerRequest.php');
+
+$config = SimpleSAML_Configuration::getInstance();
+
+/* Find the authentication state. */
+if (!array_key_exists('AuthState', $_REQUEST)) {
+	throw new SimpleSAML_Error_BadRequest('Missing mandatory parameter: AuthState');
+}
+$state = SimpleSAML_Auth_State::loadState($_REQUEST['AuthState'], 'openid:state');
+$authState = $_REQUEST['AuthState'];
+$authSource = SimpleSAML_Auth_Source::getById($state['openid:AuthId']);
+if ($authSource === NULL) {
+	throw new SimpleSAML_Error_BadRequest('Invalid AuthId \'' . $state['feide:AuthId'] . '\' - not found.');
+}
+
+
+function displayError($message) {
+    $error = $message;
+
+	$config = SimpleSAML_Configuration::getInstance();
+	$t = new SimpleSAML_XHTML_Template($config, 'openid:consumer.php', 'openid');
+	$t->data['msg'] = $msg;
+	$t->data['error'] = $error;
+	$t->show();
+}
+
+
+function &getStore() {
+    /**
+     * This is where the example will store its OpenID information.
+     * You should change this path if you want the example store to be
+     * created elsewhere.  After you're done playing with the example
+     * script, you'll have to remove this directory manually.
+     */
+    $store_path = "/tmp/_php_consumer_test";
+
+    if (!file_exists($store_path) &&
+        !mkdir($store_path)) {
+        print "Could not create the FileStore directory '$store_path'. ".
+            " Please check the effective permissions.";
+        exit(0);
+    }
+
+    return new Auth_OpenID_FileStore($store_path);
+}
+
+function &getConsumer() {
+    /**
+     * Create a consumer object using the store object created
+     * earlier.
+     */
+    $store = getStore();
+    return new Auth_OpenID_Consumer($store);
+}
+
+function getOpenIDURL() {
+    // Render a default page if we got a submission without an openid
+    // value.
+    if (empty($_GET['openid_url'])) {
+        $error = "Expected an OpenID URL.";
+
+		$config = SimpleSAML_Configuration::getInstance();
+		$t = new SimpleSAML_XHTML_Template($config, 'openid:consumer.php', 'openid');
+		$t->data['msg'] = $msg;
+		$t->data['error'] = $error;
+		$t->show();
+    }
+
+    return $_GET['openid_url'];
+}
+
+function getReturnTo() {
+	return SimpleSAML_Utilities::addURLparameter(SimpleSAML_Utilities::selfURL(), 
+		array('returned' => '1') 
+	);
+
+}
+
+function getTrustRoot() {
+	return SimpleSAML_Utilities::selfURLhost();
+}
+
+function run_try_auth() {
+    $openid = getOpenIDURL();
+    $consumer = getConsumer();
+
+    // Begin the OpenID authentication process.
+    $auth_request = $consumer->begin($openid);
+
+    // No auth request means we can't begin OpenID.
+    if (!$auth_request) {
+        displayError("Authentication error; not a valid OpenID.");
+    }
+
+    $sreg_request = Auth_OpenID_SRegRequest::build(
+			array('nickname'), // Required
+			array('fullname', 'email')); // Optional
+
+    if ($sreg_request) {
+        $auth_request->addExtension($sreg_request);
+    }
+
+    // Redirect the user to the OpenID server for authentication.
+    // Store the token for this authentication so we can verify the
+    // response.
+
+    // For OpenID 1, send a redirect.  For OpenID 2, use a Javascript
+    // form to send a POST request to the server.
+    if ($auth_request->shouldSendRedirect()) {
+        $redirect_url = $auth_request->redirectURL(getTrustRoot(), getReturnTo());
+
+        // If the redirect URL can't be built, display an error message.
+        if (Auth_OpenID::isFailure($redirect_url)) {
+            displayError("Could not redirect to server: " . $redirect_url->message);
+        } else {
+            header("Location: ".$redirect_url); // Send redirect.
+        }
+    } else {
+        // Generate form markup and render it.
+        $form_id = 'openid_message';
+        $form_html = $auth_request->formMarkup(getTrustRoot(), getReturnTo(), FALSE, array('id' => $form_id));
+
+        // Display an error if the form markup couldn't be generated; otherwise, render the HTML.
+        if (Auth_OpenID::isFailure($form_html)) {
+            displayError("Could not redirect to server: " . $form_html->message);
+        } else {
+            echo '<html><head><title>OpenID transaction in progress</title></head>
+            		<body onload=\'document.getElementById("' . $form_id . '").submit()\'>' . 
+					$form_html . '</body></html>';
+        }
+    }
+}
+
+function run_finish_auth() {
+
+	$error = 'General error. Try again.';
+
+	try {
+	
+		$consumer = getConsumer();
+	
+		// Complete the authentication process using the server's
+		// response.
+		$response = $consumer->complete();
+	
+		// Check the response status.
+		if ($response->status == Auth_OpenID_CANCEL) {
+			// This means the authentication was cancelled.
+			throw new Exception('Verification cancelled.');
+		} else if ($response->status == Auth_OpenID_FAILURE) {
+			// Authentication failed; display the error message.
+			throw new Exception("OpenID authentication failed: " . $response->message);
+		} else if ($response->status == Auth_OpenID_SUCCESS) {
+			// This means the authentication succeeded; extract the
+			// identity URL and Simple Registration data (if it was
+			// returned).
+			$openid = $response->identity_url;
+	
+			$attributes = array('openid' => array($openid));
+	
+			if ($response->endpoint->canonicalID) {
+				$attributes['openid.canonicalID'] = array($response->endpoint->canonicalID);
+			}
+	
+			$sreg_resp = Auth_OpenID_SRegResponse::fromSuccessResponse($response);
+			$sregresponse = $sreg_resp->contents();
+			
+			if (is_array($sregresponse) && count($sregresponse) > 0) {
+				$attributes['openid.sregkeys'] = array_keys($sregresponse);
+				foreach ($sregresponse AS $sregkey => $sregvalue) {
+					$attributes['openid.sreg.' . $sregkey] = array($sregvalue);
+				}
+			}
+
+			global $state;
+			$state['Attributes'] = $attributes;
+			SimpleSAML_Auth_Source::completeAuth($state);
+			
+		}
+
+	} catch (Exception $e) {
+		$error = $e->getMessage();
+	}
+
+	$config = SimpleSAML_Configuration::getInstance();
+	$t = new SimpleSAML_XHTML_Template($config, 'openid:consumer.php', 'openid');
+	$t->data['error'] = $error;
+	global $authState;
+	$t->data['AuthState'] = $authState;
+	$t->show();
+
+}
+
+if (array_key_exists('returned', $_GET)) {
+	run_finish_auth();
+} elseif(array_key_exists('openid_url', $_GET)) {
+	run_try_auth();
+} else {
+	$config = SimpleSAML_Configuration::getInstance();
+	$t = new SimpleSAML_XHTML_Template($config, 'openid:consumer.php', 'openid');
+	global $authState;
+	$t->data['AuthState'] = $authState;
+	$t->show();
+}
+
+
+
+?>
\ No newline at end of file
diff --git a/modules/openid/www/openidtest.php b/modules/openid/www/openidtest.php
new file mode 100644
index 000000000..4f6d946ac
--- /dev/null
+++ b/modules/openid/www/openidtest.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * The _include script registers a autoloader for the simpleSAMLphp libraries. It also
+ * initializes the simpleSAMLphp config class with the correct path.
+ */
+require_once('_include.php');
+
+
+/* Load simpleSAMLphp, configuration and metadata */
+$config = SimpleSAML_Configuration::getInstance();
+$session = SimpleSAML_Session::getInstance();
+
+if (! $session->isValid('openid') ) {
+	/* Authenticate with an AuthSource. */
+	$hints = array('openid' => NULL);
+	SimpleSAML_Auth_Default::initLogin('openid', SimpleSAML_Utilities::selfURL(), NULL, $hints);
+}
+
+$attributes = $session->getAttributes();
+
+$t = new SimpleSAML_XHTML_Template($config, 'status.php', 'attributes');
+$t->data['header'] = '{openid:dictopenid:openidtestpage}';
+$t->data['remaining'] = $session->remainingTime();
+$t->data['sessionsize'] = $session->getSize();
+$t->data['attributes'] = $attributes;
+$t->data['icon'] = 'bino.png';
+$t->data['logouturl'] = NULL;
+$t->show();
+
+
+?>
\ No newline at end of file
diff --git a/www/resources/icons/openid.png b/www/resources/icons/openid.png
new file mode 100644
index 0000000000000000000000000000000000000000..502b3577a5b09803db28a30cbcf0f1651c089921
GIT binary patch
literal 3870
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

literal 0
HcmV?d00001

-- 
GitLab