summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDries Buytaert <dries@buytaert.net>2007-06-18 16:09:39 +0000
committerDries Buytaert <dries@buytaert.net>2007-06-18 16:09:39 +0000
commitaa308028aa570d15d3abf9c7679d7de16019b78b (patch)
treec7f87e5423007f088feb99149ebf2d16752631be
parent8f9298577e7db9af6d34c3d69bd633a5c7e3de74 (diff)
downloadbrdo-aa308028aa570d15d3abf9c7679d7de16019b78b.tar.gz
brdo-aa308028aa570d15d3abf9c7679d7de16019b78b.tar.bz2
- Patch #131026 by James et al: OpenID client support for Drupal!
Let this be the day where we help revolutionize the online society, and the way websites and web services interoperate. Or something.
-rw-r--r--CHANGELOG.txt1
-rw-r--r--modules/locale/locale.module8
-rw-r--r--modules/node/node.module2
-rw-r--r--modules/openid/login-bg.pngbin0 -> 426 bytes
-rw-r--r--modules/openid/openid.css32
-rw-r--r--modules/openid/openid.inc420
-rw-r--r--modules/openid/openid.info6
-rw-r--r--modules/openid/openid.install18
-rw-r--r--modules/openid/openid.js29
-rw-r--r--modules/openid/openid.module530
-rw-r--r--modules/openid/openid.schema19
-rw-r--r--modules/openid/xrds.inc79
12 files changed, 1139 insertions, 5 deletions
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 3f8ed3a52..66b04db8a 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -57,6 +57,7 @@ Drupal 6.0, xxxx-xx-xx (development version)
- File handling improvements:
* Entries in the files table are now keyed to a user, and not a node.
* Added re-usable validation functions to check for uploaded file sizes, extensions, and image resolution.
+- Added support for OpenID.
Drupal 5.0, 2007-01-15
----------------------
diff --git a/modules/locale/locale.module b/modules/locale/locale.module
index 86831fa6c..e296f68e8 100644
--- a/modules/locale/locale.module
+++ b/modules/locale/locale.module
@@ -285,7 +285,7 @@ function locale_theme() {
*
* @param $string
* A string to look up translation for. If omitted, all the
- * cached strings will be returned in all languages already
+ * cached strings will be returned in all languages already
* used on the page.
* @param $langcode
* Language code to use for the lookup.
@@ -304,9 +304,9 @@ function locale($string = NULL, $langcode = NULL) {
// Store database cached translations in a static var.
if (!isset($locale_t[$langcode])) {
$locale_t[$langcode] = array();
- // Disabling the usage of string caching allows a module to watch for
- // the exact list of strings used on a page. From a performance
- // perspective that is a really bad idea, so we have no user
+ // Disabling the usage of string caching allows a module to watch for
+ // the exact list of strings used on a page. From a performance
+ // perspective that is a really bad idea, so we have no user
// interface for this. Be careful when turning this option off!
if (variable_get('locale_cache_strings', 1) == 1) {
if (!($cache = cache_get('locale:'. $langcode, 'cache'))) {
diff --git a/modules/node/node.module b/modules/node/node.module
index 091adbdc7..c253610d9 100644
--- a/modules/node/node.module
+++ b/modules/node/node.module
@@ -1383,7 +1383,7 @@ function node_filters() {
}
$filters['type'] = array('title' => t('type'), 'options' => node_get_types('names'));
-
+
// The taxonomy filter
if ($taxonomy = module_invoke('taxonomy', 'form_all', 1)) {
$filters['category'] = array('title' => t('category'), 'options' => $taxonomy);
diff --git a/modules/openid/login-bg.png b/modules/openid/login-bg.png
new file mode 100644
index 000000000..8eca055f3
--- /dev/null
+++ b/modules/openid/login-bg.png
Binary files differ
diff --git a/modules/openid/openid.css b/modules/openid/openid.css
new file mode 100644
index 000000000..e6c30df2a
--- /dev/null
+++ b/modules/openid/openid.css
@@ -0,0 +1,32 @@
+a.openid-link, a.user-link, #edit-openid-url {
+ background-image: url("login-bg.png");
+ background-position: 0% 50%;
+ background-repeat: no-repeat;
+ padding-left: 20px;
+}
+
+div#edit-openid-url-wrapper {
+ display: block;
+}
+
+html.js #user-login-form div#edit-openid-url-wrapper,
+html.js #user-login div#edit-openid-url-wrapper {
+ display: none;
+}
+
+html.js #user-login-form a.openid-link,
+html.js #user-login a.openid-link {
+ display : block;
+}
+
+#user-login-form a.openid-link,
+#user-login-form a.user-link,
+#user-login a.openid-link,
+#user-login a.user-link {
+ display: none;
+}
+
+#user-login-form a.openid-link,
+#user-login-form a.user-link {
+ text-align : left;
+}
diff --git a/modules/openid/openid.inc b/modules/openid/openid.inc
new file mode 100644
index 000000000..e54758a3c
--- /dev/null
+++ b/modules/openid/openid.inc
@@ -0,0 +1,420 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * OpenID utility functions.
+ */
+
+// Diffie-Hellman Key Exchange Default Value.
+define('OPENID_DH_DEFAULT_MOD', '155172898181473697471232257763715539915724801'.
+ '966915404479707795314057629378541917580651227423698188993727816152646631'.
+ '438561595825688188889951272158842675419950341258706556549803580104870537'.
+ '681476726513255747040765857479291291572334510643245094715007229621094194'.
+ '349783925984760375594985848253359305585439638443');
+
+// Constants for Diffie-Hellman key exchange computations.
+define('OPENID_DH_DEFAULT_GEN', '2');
+define('OPENID_SHA1_BLOCKSIZE', 64);
+define('OPENID_RAND_SOURCE', '/dev/urandom');
+
+// OpenID namespace URLs
+define('OPENID_NS_2_0', 'http://specs.openid.net/auth/2.0');
+define('OPENID_NS_1_1', 'http://openid.net/signon/1.1');
+define('OPENID_NS_1_0', 'http://openid.net/signon/1.0');
+
+/**
+ * Performs an HTTP 302 redirect (for the 1.x protocol).
+ */
+function openid_redirect_http($url, $message) {
+ $query = array();
+ foreach ($message as $key => $val) {
+ $query[] = $key .'='. urlencode($val);
+ }
+
+ $sep = (strpos($url, '?') === FALSE) ? '?' : '&';
+ header('Location: ' . $url . $sep . implode('&', $query), TRUE, 302);
+ exit;
+}
+
+/**
+ * Creates a js auto-submit redirect for (for the 2.x protocol)
+ */
+function openid_redirect($url, $message) {
+ $output = '<html><head><title>'.t('OpenID redirect'). "</title></head>\n<body>";
+ $output .= drupal_get_form('openid_redirect_form', $url, $message);
+ $output .= '<script type="text/javascript">document.getElementById("openid-redirect-form").submit();</script>';
+ $output .= "</body></html>\n";
+ print $output;
+ exit;
+}
+
+function openid_redirect_form(&$form_state, $url, $message) {
+ $form = array();
+ $form['#action'] = $url;
+ $form['#method'] = "post";
+ foreach ($message as $key => $value) {
+ $form[$key] = array(
+ '#type' => 'hidden',
+ '#name' => $key,
+ '#value' => $value,
+ );
+ }
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#prefix' => '<noscript>',
+ '#suffix' => '</noscript>',
+ '#value' => t('Send'),
+ );
+
+ return $form;
+}
+
+/**
+ * Determine if the given identifier is an XRI ID.
+ */
+function _openid_is_xri($identifier) {
+ $firstchar = substr($identifier, 0, 1);
+ if ($firstchar == "@" || $firstchar == "=")
+ return TRUE;
+
+ if (stristr($identifier, 'xri://') !== FALSE) {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/**
+ * Normalize the given identifer as per spec.
+ */
+function _openid_normalize($identifier) {
+ if (_openid_is_xri($identifier)) {
+ return _openid_normalize_xri($identifier);
+ }
+ else {
+ return _openid_normalize_url($identifier);
+ }
+}
+
+function _openid_normalize_xri($xri) {
+ $normalized_xri = $xri;
+ if (stristr($xri, 'xri://') !== FALSE) {
+ $normalized_xri = substr($xri, 6);
+ }
+ return $normalized_xri;
+}
+
+function _openid_normalize_url($url) {
+ $normalized_url = $url;
+
+ if (stristr($url, '://') === FALSE) {
+ $normalized_url = 'http://' . $url;
+ }
+
+ if (substr_count($normalized_url, '/') < 3) {
+ $normalized_url .= '/';
+ }
+
+ return $normalized_url;
+}
+
+/**
+ * Create a serialized message packet as per spec: $key:$value\n .
+ */
+function _openid_create_message($data) {
+ $serialized = '';
+
+ foreach ($data as $key => $value) {
+ if ((strpos($key, ':') !== FALSE) || (strpos($key, "\n") !== FALSE) || (strpos($value, "\n") !== FALSE)) {
+ return null;
+ }
+ $serialized .= "$key:$value\n";
+ }
+ return $serialized;
+}
+
+/**
+ * Encode a message from _openid_create_message for HTTP Post
+ */
+function _openid_encode_message($message) {
+ $encoded_message = '';
+
+ $items = explode("\n", $message);
+ foreach ($items as $item) {
+ $parts = explode(':', $item, 2);
+
+ if (count($parts) == 2) {
+ if ($encoded_message != '') {
+ $encoded_message .= '&';
+ }
+ $encoded_message .= rawurlencode(trim($parts[0])) . '=' . rawurlencode(trim($parts[1]));
+ }
+ }
+
+ return $encoded_message;
+}
+
+/**
+ * Convert a direct communication message
+ * into an associative array.
+ */
+function _openid_parse_message($message) {
+ $parsed_message = array();
+
+ $items = explode("\n", $message);
+ foreach ($items as $item) {
+ $parts = explode(':', $item, 2);
+
+ if (count($parts) == 2) {
+ $parsed_message[$parts[0]] = $parts[1];
+ }
+ }
+
+ return $parsed_message;
+}
+
+/**
+ * Return a nonce value - formatted per OpenID spec.
+ */
+function _openid_nonce() {
+ // YYYY-MM-DDThh:mm:ssTZD UTC, plus some optional extra unique chars
+ return gmstrftime('%Y-%m-%dT%H:%M:%S%Z') .
+ chr(mt_rand(0, 25) + 65) .
+ chr(mt_rand(0, 25) + 65) .
+ chr(mt_rand(0, 25) + 65) .
+ chr(mt_rand(0, 25) + 65);
+}
+
+/**
+ * Pull the href attribute out of an html link element.
+ */
+function _openid_link_href($rel, $html) {
+ $rel = preg_quote($rel);
+ preg_match('|<link\s+rel=["\'](.*)'. $rel .'(.*)["\'](.*)/?>|iU', $html, $matches);
+ if (isset($matches[3])) {
+ preg_match('|href=["\']([^"]+)["\']|iU', $matches[0], $href);
+ return trim($href[1]);
+ }
+ return FALSE;
+}
+
+/**
+ * Pull the http-equiv attribute out of an html meta element
+ */
+function _openid_meta_httpequiv($equiv, $html) {
+ preg_match('|<meta\s+http-equiv=["\']' . $equiv . '["\'](.*)/?>|iU', $html, $matches);
+ if (isset($matches[1])) {
+ preg_match('|content=["\']([^"]+)["\']|iU', $matches[1], $content);
+ return $content[1];
+ }
+ return FALSE;
+}
+
+/**
+ * Sign certain keys in a message
+ * @param $association - object loaded from openid_association or openid_server_association table
+ * - important fields are ->assoc_type and ->mac_key
+ * @param $message_array - array of entire message about to be sent
+ * @param $keys_to_sign - keys in the message to include in signature (without
+ * 'openid.' appended)
+ */
+function _openid_signature($association, $message_array, $keys_to_sign) {
+ $signature = '';
+ $sign_data = array();
+
+ foreach ($keys_to_sign as $key) {
+ if (isset($message_array['openid.' . $key])) {
+ $sign_data[$key] = $message_array['openid.' . $key];
+ }
+ }
+
+ $message = _openid_create_message($sign_data);
+ $secret = base64_decode($association->mac_key);
+ $signature = _openid_hmac($secret, $message);
+
+ return base64_encode($signature);
+}
+
+function _openid_hmac($key, $text) {
+ if (strlen($key) > OPENID_SHA1_BLOCKSIZE) {
+ $key = _openid_sha1($key, true);
+ }
+
+ $key = str_pad($key, OPENID_SHA1_BLOCKSIZE, chr(0x00));
+ $ipad = str_repeat(chr(0x36), OPENID_SHA1_BLOCKSIZE);
+ $opad = str_repeat(chr(0x5c), OPENID_SHA1_BLOCKSIZE);
+ $hash1 = _openid_sha1(($key ^ $ipad) . $text, true);
+ $hmac = _openid_sha1(($key ^ $opad) . $hash1, true);
+
+ return $hmac;
+}
+
+function _openid_sha1($text) {
+ $hex = sha1($text);
+ $raw = '';
+ for ($i = 0; $i < 40; $i += 2) {
+ $hexcode = substr($hex, $i, 2);
+ $charcode = (int)base_convert($hexcode, 16, 10);
+ $raw .= chr($charcode);
+ }
+ return $raw;
+}
+
+function _openid_dh_base64_to_long($str) {
+ $b64 = base64_decode($str);
+
+ return _openid_dh_binary_to_long($b64);
+}
+
+function _openid_dh_long_to_base64($str) {
+ return base64_encode(_openid_dh_long_to_binary($str));
+}
+
+function _openid_dh_binary_to_long($str) {
+ $bytes = array_merge(unpack('C*', $str));
+
+ $n = 0;
+ foreach ($bytes as $byte) {
+ $n = bcmul($n, pow(2, 8));
+ $n = bcadd($n, $byte);
+ }
+
+ return $n;
+}
+
+function _openid_dh_long_to_binary($long) {
+ $cmp = bccomp($long, 0);
+ if ($cmp < 0) {
+ return FALSE;
+ }
+
+ if ($cmp == 0) {
+ return "\x00";
+ }
+
+ $bytes = array();
+
+ while (bccomp($long, 0) > 0) {
+ array_unshift($bytes, bcmod($long, 256));
+ $long = bcdiv($long, pow(2, 8));
+ }
+
+ if ($bytes && ($bytes[0] > 127)) {
+ array_unshift($bytes, 0);
+ }
+
+ $string = '';
+ foreach ($bytes as $byte) {
+ $string .= pack('C', $byte);
+ }
+
+ return $string;
+}
+
+function _openid_dh_xorsecret($shared, $secret) {
+ $dh_shared_str = _openid_dh_long_to_binary($shared);
+ $sha1_dh_shared = _openid_sha1($dh_shared_str);
+ $xsecret = "";
+ for ($i = 0; $i < strlen($secret); $i++) {
+ $xsecret .= chr(ord($secret[$i]) ^ ord($sha1_dh_shared[$i]));
+ }
+
+ return $xsecret;
+}
+
+function _openid_dh_rand($stop) {
+ static $duplicate_cache = array();
+
+ // Used as the key for the duplicate cache
+ $rbytes = _openid_dh_long_to_binary($stop);
+
+ if (array_key_exists($rbytes, $duplicate_cache)) {
+ list($duplicate, $nbytes) = $duplicate_cache[$rbytes];
+ }
+ else {
+ if ($rbytes[0] == "\x00") {
+ $nbytes = strlen($rbytes) - 1;
+ }
+ else {
+ $nbytes = strlen($rbytes);
+ }
+
+ $mxrand = bcpow(256, $nbytes);
+
+ // If we get a number less than this, then it is in the
+ // duplicated range.
+ $duplicate = bcmod($mxrand, $stop);
+
+ if (count($duplicate_cache) > 10) {
+ $duplicate_cache = array();
+ }
+
+ $duplicate_cache[$rbytes] = array($duplicate, $nbytes);
+ }
+
+ do {
+ $bytes = "\x00" . _openid_get_bytes($nbytes);
+ $n = _openid_dh_binary_to_long($bytes);
+ // Keep looping if this value is in the low duplicated range.
+ } while (bccomp($n, $duplicate) < 0);
+
+ return bcmod($n, $stop);
+}
+
+function _openid_get_bytes($num_bytes) {
+ static $f = null;
+ $bytes = '';
+ if (!isset($f)) {
+ $f = @fopen(OPENID_RAND_SOURCE, "r");
+ }
+ if (!isset($f)) {
+ // pseudorandom used
+ $bytes = '';
+ for ($i = 0; $i < $num_bytes; $i += 4) {
+ $bytes .= pack('L', mt_rand());
+ }
+ $bytes = substr($bytes, 0, $num_bytes);
+ }
+ else {
+ $bytes = fread($f, $num_bytes);
+ }
+ return $bytes;
+}
+
+/**
+ * Fix PHP's habit of replacing '.' by '_' in posted data.
+ */
+function _openid_fix_post(&$post) {
+ $extensions = module_invoke_all('openid', 'extension');
+ foreach ($post as $key => $value) {
+ if (strpos($key, 'openid_') === 0) {
+ $fixed_key = str_replace('openid_', 'openid.', $key);
+ $fixed_key = str_replace('openid.ns_', 'openid.ns.', $fixed_key);
+ $fixed_key = str_replace('openid.sreg_', 'openid.sreg.', $fixed_key);
+ foreach ($extensions as $ext) {
+ $fixed_key = str_replace('openid.'.$ext.'_', 'openid.'.$ext.'.', $fixed_key);
+ }
+ unset($post[$key]);
+ $post[$fixed_key] = $value;
+ }
+ }
+}
+
+/**
+ * Provide bcpowmod support for PHP4.
+ */
+if (!function_exists('bcpowmod')) {
+ function bcpowmod($base, $exp, $mod) {
+ $square = bcmod($base, $mod);
+ $result = 1;
+ while (bccomp($exp, 0) > 0) {
+ if (bcmod($exp, 2)) {
+ $result = bcmod(bcmul($result, $square), $mod);
+ }
+ $square = bcmod(bcmul($square, $square), $mod);
+ $exp = bcdiv($exp, 2);
+ }
+ return $result;
+ }
+} \ No newline at end of file
diff --git a/modules/openid/openid.info b/modules/openid/openid.info
new file mode 100644
index 000000000..724434164
--- /dev/null
+++ b/modules/openid/openid.info
@@ -0,0 +1,6 @@
+; $Id$
+name = OpenID
+description = "Allows Drupal to act as an OpenID relying party."
+version = VERSION
+package = Core - optional
+core = 6.x
diff --git a/modules/openid/openid.install b/modules/openid/openid.install
new file mode 100644
index 000000000..c98b9ea34
--- /dev/null
+++ b/modules/openid/openid.install
@@ -0,0 +1,18 @@
+<?php
+// $Id$
+
+/**
+ * Implementation of hook_install().
+ */
+function openid_install() {
+ // Create table.
+ drupal_install_schema('openid');
+}
+
+/**
+ * Implementation of hook_uninstall().
+ */
+function openid_uninstall() {
+ // Remove table.
+ drupal_uninstall_schema('openid');
+}
diff --git a/modules/openid/openid.js b/modules/openid/openid.js
new file mode 100644
index 000000000..5f27b66f5
--- /dev/null
+++ b/modules/openid/openid.js
@@ -0,0 +1,29 @@
+// $Id$
+
+$(document).ready(
+ function() {
+ if ($("#edit-openid-url").val()) {
+ $("#edit-name-wrapper").hide();
+ $("#edit-pass-wrapper").hide();
+ $("#edit-openid-url-wrapper").show();
+ $("a.openid-link").hide();
+ }
+ $("a.openid-link").click( function() {
+ $("#edit-pass-wrapper").hide();
+ $("#edit-name-wrapper").fadeOut('medium', function() {
+ $("#edit-openid-url-wrapper").fadeIn('medium');
+ });
+ $("a.openid-link").hide();
+ $("a.user-link").show();
+ return false;
+ });
+ $("a.user-link").click( function() {
+ $("#edit-openid-url-wrapper").hide();
+ $("#edit-pass-wrapper").show();
+ $("#edit-name-wrapper").show();
+ $("a.user-link").hide();
+ $("a.openid-link").show();
+ return false;
+ });
+ });
+
diff --git a/modules/openid/openid.module b/modules/openid/openid.module
new file mode 100644
index 000000000..5be1fb137
--- /dev/null
+++ b/modules/openid/openid.module
@@ -0,0 +1,530 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Implement OpenID Relying Party support for Drupal
+ */
+
+/**
+ * Implementation of hook_menu.
+ */
+function openid_menu() {
+ $items['openid/authenticate'] = array(
+ 'title' => 'OpenID Login',
+ 'page callback' => 'openid_authentication_page',
+ 'access callback' => 'user_is_anonymous',
+ 'type' => MENU_CALLBACK,
+ );
+ $items['user/%user/openid'] = array(
+ 'title' => 'OpenID identities',
+ 'page callback' => 'openid_user_identities',
+ 'page arguments' => array(1),
+ 'access callback' => 'user_edit_access',
+ 'access arguments' => array(1),
+ 'type' => MENU_LOCAL_TASK,
+ );
+ $items['user/%user/openid/delete'] = array(
+ 'title' => 'Delete OpenID',
+ 'page callback' => 'openid_user_delete',
+ 'page arguments' => array(1),
+ 'type' => MENU_CALLBACK,
+ );
+ return $items;
+}
+
+/**
+ * Implementation of hook_help().
+ */
+function openid_help($section) {
+ switch ($section) {
+ case 'user/%/openid':
+ return t('You may login to this site using an OpenID. You may add your OpenId URLs below, and also see a list of any OpenIDs which have already been added.');
+ }
+}
+
+/**
+ * Implementation of hook_user().
+ */
+function openid_user($op, &$edit, &$account, $category = NULL) {
+ if ($op == 'insert' && isset($_SESSION['openid'])) {
+ // The user has registered after trying to login via OpenID.
+ if (variable_get('user_email_verification', TRUE)) {
+ drupal_set_message(t('Once you have verified your email address, you may log in via OpenID.'));
+ }
+ unset($_SESSION['openid']);
+ }
+}
+
+/**
+ * Implementation of hook_form_alter : adds OpenID login to the login forms.
+ */
+function openid_form_alter(&$form, $form_state, $form_id) {
+ if ($form_id == 'user_login_block' || $form_id == 'user_login') {
+ drupal_add_css(drupal_get_path('module', 'openid') .'/openid.css', 'module');
+ drupal_add_js(drupal_get_path('module', 'openid') .'/openid.js');
+ if (!empty($form_state['post']['openid_url'])) {
+ $form['name']['#required'] = FALSE;
+ $form['pass']['#required'] = FALSE;
+ unset($form['#submit']);
+ $form['#validate'] = array('openid_login_validate');
+ }
+
+ $form['openid_link'] = array('#value' => l(t('Log in using OpenID'), '#', array('attributes' => array('class' => 'openid-link'))));
+ $form['user_link'] = array('#value' => l(t('Cancel OpenID login'), '#', array('attributes' => array('class' => 'user-link'))));
+
+ $form['openid_url'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Log in using OpenID'),
+ '#size' => ($form_id == 'user_login') ? 58 : 13,
+ '#maxlength' => 255,
+ '#weight' => -1,
+ '#description' => l(t('What is OpenID?'), 'http://openid.net/', array('external' => TRUE))
+ );
+ $form['openid.return_to'] = array('#type' => 'hidden', '#value' => url('openid/authenticate', array('absolute' => TRUE, 'query' => drupal_get_destination())));
+ }
+ elseif ($form_id == 'user_register' && isset($_SESSION['openid'])) {
+ // We were unable to auto-register a new user. Prefill the registration
+ // form with the values we have.
+ $form['name']['#default_value'] = $_SESSION['openid']['name'];
+ $form['mail']['#default_value'] = $_SESSION['openid']['mail'];
+ // If user_email_verification is off, hide the password field and just fill
+ // with random password to avoid confusion.
+ if (!variable_get('user_email_verification', TRUE)) {
+ $form['pass']['#type'] = 'hidden';
+ $form['pass']['#value'] = user_password();
+ }
+ $form['auth_openid'] = array('#type' => 'hidden', '#value' => $_SESSION['openid']['auth_openid']);
+ }
+ return $form;
+}
+
+/**
+ * Login form _validate hook
+ */
+function openid_login_validate($form, &$form_state) {
+ $return_to = $form_state['values']['openid.return_to'];
+ if (empty($return_to)) {
+ $return_to = url('', array('absolute' => TRUE));
+ }
+
+ openid_begin($form_state['values']['openid_url'], $return_to);
+}
+
+/**
+ * Callbacks.
+ */
+function openid_authentication_page() {
+ $result = openid_complete($_REQUEST);
+ switch ($result['status']) {
+ case 'success':
+ return openid_authentication($result);
+ case 'failed':
+ drupal_set_message(t('OpenID login failed.'), 'error');
+ break;
+ case 'cancel':
+ drupal_set_message(t('OpenID login cancelled.'));
+ break;
+ }
+ drupal_goto();
+}
+
+function openid_user_identities($account) {
+ drupal_add_css(drupal_get_path('module', 'openid') .'/openid.css', 'module');
+
+ // Check to see if we got a response
+ $result = openid_complete($_REQUEST);
+ if ($result['status'] == 'success') {
+ db_query("INSERT INTO {authmap} (uid, authname, module) VALUES (%d, '%s','openid')", $account->uid, $result['openid.identity']);
+ drupal_set_message(t('Successfully added %identity', array('%identity' => $result['openid.identity'])));
+ }
+
+ $header = array(t('OpenID'), t('Operations'));
+ $rows = array();
+
+ $result = db_query("SELECT * FROM {authmap} WHERE module='openid' AND uid=%d", $account->uid);
+ while ($identity = db_fetch_object($result)) {
+ $rows[] = array($identity->authname, l(t('Delete'), 'user/'. $account->uid .'/openid/delete/'. $identity->aid));
+ }
+
+ $output = theme('table', $header, $rows);
+ $output .= drupal_get_form('openid_user_add');
+ return $output;
+}
+
+function openid_user_add() {
+ $form['openid_url'] = array(
+ '#type' => 'textfield',
+ '#title' => t('OpenID'),
+ );
+ $form['submit'] = array('#type' => 'submit', '#value' => t('Add an OpenID'));
+ return $form;
+}
+
+function openid_user_add_validate($form, &$form_state) {
+ // Check for existing entries.
+ $claimed_id = _openid_normalize($form_state['values']['openid_url']);
+ if (db_result(db_query("SELECT authname FROM {authmap} WHERE authname='%s'", $claimed_id))) {
+ form_set_error('openid_url', t('That OpenID is already in use on this site.'));
+ }
+ else {
+ $return_to = url('user/'. arg(1) .'/openid', array('absolute' => TRUE));
+ openid_begin($form_state['values']['openid_url'], $return_to);
+ }
+}
+
+function openid_user_delete($account, $aid = 0) {
+ db_query("DELETE FROM {authmap} WHERE uid=%d AND aid=%d AND module='openid'", $account->uid, $aid);
+ if (db_affected_rows()) {
+ drupal_set_message(t('OpenID deleted.'));
+ }
+ drupal_goto('user/'. $account->uid .'/openid');
+}
+
+/**
+ * The initial step of OpenID authentication responsible for the following:
+ * - Perform discovery on the claimed OpenID.
+ * - If possible, create an association with the Provider's endpoint.
+ * - Create the authentication request.
+ * - Perform the appropriate redirect.
+ *
+ * @param $claimed_id The OpenID to authenticate
+ * @param $return_to The endpoint to return to from the OpenID Provider
+ */
+function openid_begin($claimed_id, $return_to = '') {
+ include_once drupal_get_path('module', 'openid') .'/openid.inc';
+
+ $claimed_id = _openid_normalize($claimed_id);
+
+ $services = openid_discovery($claimed_id);
+ if (count($services) == 0) {
+ form_set_error('openid_url', t('Sorry, that is not a valid OpenID. Please ensure you have spelled your ID correctly.'));
+ return;
+ }
+
+ $op_endpoint = $services[0]['uri'];
+ // Store the discovered endpoint in the session (so we don't have to rediscover).
+ $_SESSION['openid_op_endpoint'] = $op_endpoint;
+ // Store the claimed_id in the session (for handling delegation).
+ $_SESSION['openid_claimed_id'] = $claimed_id;
+
+ // If bcmath is present, then create an association
+ $assoc_handle = '';
+ if (function_exists('bcadd')) {
+ $assoc_handle = openid_association($op_endpoint);
+ }
+
+ // Now that there is an association created, move on
+ // to request authentication from the IdP
+ $identity = (!empty($services[0]['delegate'])) ? $services[0]['delegate'] : $claimed_id;
+ if (isset($services[0]['types']) && is_array($services[0]['types']) && in_array(OPENID_NS_2_0 .'/server', $services[0]['types'])) {
+ $identity = 'http://openid.net/identifier_select/2.0';
+ }
+ $authn_request = openid_authentication_request($claimed_id, $identity, $return_to, $assoc_handle, $services[0]['version']);
+
+ if ($services[0]['version'] == 2) {
+ openid_redirect($op_endpoint, $authn_request);
+ }
+ else {
+ openid_redirect_http($op_endpoint, $authn_request);
+ }
+}
+
+/**
+ * Completes OpenID authentication by validating returned data from the OpenID
+ * Provider.
+ *
+ * @param $response Array of returned from the OpenID provider (typically $_REQUEST).
+ *
+ * @return $response Response values for further processing with
+ * $response['status'] set to one of 'success', 'failed' or 'cancel'.
+ */
+function openid_complete($response) {
+ include_once drupal_get_path('module', 'openid') .'/openid.inc';
+
+ // Default to failed response
+ $response['status'] = 'failed';
+ if (isset($_SESSION['openid_op_endpoint']) && isset($_SESSION['openid_claimed_id'])) {
+ _openid_fix_post($response);
+ $op_endpoint = $_SESSION['openid_op_endpoint'];
+ $claimed_id = $_SESSION['openid_claimed_id'];
+ unset($_SESSION['openid_op_endpoint']);
+ unset($_SESSION['openid_claimed_id']);
+ if (isset($response['openid.mode'])) {
+ if ($response['openid.mode'] == 'cancel') {
+ $response['status'] = 'cancel';
+ }
+ else {
+ if (openid_verify_assertion($op_endpoint, $response)) {
+ $response['openid.identity'] = $claimed_id;
+ $response['status'] = 'success';
+ }
+ }
+ }
+ }
+ return $response;
+}
+
+/**
+ * Perform discovery on a claimed ID to determine the OpenID provider endpoint.
+ *
+ * @param $claimed_id The OpenID URL to perform discovery on.
+ *
+ * @return Array of services discovered (including OpenID version, endpoint
+ * URI, etc).
+ */
+function openid_discovery($claimed_id) {
+ include_once drupal_get_path('module', 'openid') .'/openid.inc';
+ include_once drupal_get_path('module', 'openid') .'/xrds.inc';
+
+ $services = array();
+
+ $xrds_url = $claimed_id;
+ if (_openid_is_xri($claimed_id)) {
+ $xrds_url = 'http://xri.net/'. $claimed_id;
+ }
+ $url = @parse_url($xrds_url);
+ if ($url['scheme'] == 'http' || $url['scheme'] == 'https') {
+ // For regular URLs, try Yadis resolution first, then HTML-based discovery
+ $headers = array('Accept' => 'application/xrds+xml');
+ $result = drupal_http_request($xrds_url, $headers);
+
+ if (!isset($result->error)) {
+ if (isset($result->headers['Content-Type']) && preg_match("/application\/xrds\+xml/", $result->headers['Content-Type'])) {
+ // Parse XML document to find URL
+ $services = xrds_parse($result->data);
+ }
+ else {
+ $xrds_url = NULL;
+ if (isset($result->headers['X-XRDS-Location'])) {
+ $xrds_url = $result->headers['X-XRDS-Location'];
+ }
+ else {
+ // Look for meta http-equiv link in HTML head
+ $xrds_url = _openid_meta_httpequiv('X-XRDS-Location', $result->data);
+ }
+ if (!empty($xrds_url)) {
+ $headers = array('Accept' => 'application/xrds+xml');
+ $xrds_result = drupal_http_request($xrds_url, $headers);
+ if (!isset($xrds_result->error)) {
+ $services = xrds_parse($xrds_result->data);
+ }
+ }
+ }
+
+ // Check for HTML delegation
+ if (count($services) == 0) {
+ // Look for 2.0 links
+ $uri = _openid_link_href('openid2.provider', $result->data);
+ $delegate = _openid_link_href('openid2.local_id', $result->data);
+ $version = 2;
+
+ // 1.0 links
+ if (empty($uri)) {
+ $uri = _openid_link_href('openid.server', $result->data);
+ $delegate = _openid_link_href('openid.delegate', $result->data);
+ $version = 1;
+ }
+ if (!empty($uri)) {
+ $services[] = array('uri' => $uri, 'delegate' => $delegate, 'version' => $version);
+ }
+ }
+ }
+ }
+ return $services;
+}
+
+/**
+ * Attempt to create a shared secret with the OpenID Provider.
+ *
+ * @param $op_endpoint URL of the OpenID Provider endpoint.
+ *
+ * @return $assoc_handle The association handle.
+ */
+function openid_association($op_endpoint) {
+ include_once drupal_get_path('module', 'openid') .'/openid.inc';
+
+ // Remove Old Associations:
+ db_query("DELETE FROM {openid_association} WHERE created + expires_in < %d", time());
+
+ // Check to see if we have an association for this IdP already
+ $assoc_handle = db_result(db_query("SELECT assoc_handle FROM {openid_association} WHERE idp_endpoint_uri = '%s'", $op_endpoint));
+ if (empty($assoc_handle)) {
+ $mod = OPENID_DH_DEFAULT_MOD;
+ $gen = OPENID_DH_DEFAULT_GEN;
+ $r = _openid_dh_rand($mod);
+ $private = bcadd($r, 1);
+ $public = bcpowmod($gen, $private, $mod);
+
+ // If there is no existing association, then request one
+ $assoc_request = openid_association_request($public);
+ $assoc_message = _openid_encode_message(_openid_create_message($assoc_request));
+ $assoc_headers = array('Content-Type' => 'application/x-www-form-urlencoded; charset=utf-8');
+ $assoc_result = drupal_http_request($op_endpoint, $assoc_headers, 'POST', $assoc_message);
+ if (isset($assoc_result->error)) {
+ return FALSE;
+ }
+
+ $assoc_response = _openid_parse_message($assoc_result->data);
+ if (isset($assoc_response['mode']) && $assoc_response['mode'] == 'error') {
+ return FALSE;
+ }
+
+ if ($assoc_response['session_type'] == 'DH-SHA1') {
+ $spub = _openid_dh_base64_to_long($assoc_response['dh_server_public']);
+ $enc_mac_key = base64_decode($assoc_response['enc_mac_key']);
+ $shared = bcpowmod($spub, $private, $mod);
+ $assoc_response['mac_key'] = base64_encode(_openid_dh_xorsecret($shared, $enc_mac_key));
+ }
+ db_query("INSERT INTO {openid_association} (idp_endpoint_uri, session_type, assoc_handle, assoc_type, expires_in, mac_key, created) VALUES('%s', '%s', '%s', '%s', %d, '%s', %d)",
+ $op_endpoint, $assoc_response['session_type'], $assoc_response['assoc_handle'], $assoc_response['assoc_type'], $assoc_response['expires_in'], $assoc_response['mac_key'], time());
+
+ $assoc_handle = $assoc_response['assoc_handle'];
+ }
+
+ return $assoc_handle;
+}
+
+/**
+ * Authenticate a user or attempt registration.
+ *
+ * @param $response Response values from the OpenID Provider.
+ */
+function openid_authentication($response) {
+ include_once drupal_get_path('module', 'openid') .'/openid.inc';
+
+ $identity = $response['openid.identity'];
+
+ $account = user_external_load($identity);
+ if (isset($account->uid)) {
+ if (!variable_get('user_email_verification', TRUE) || $account->login) {
+ user_external_login($account);
+ }
+ else {
+ drupal_set_message(t('You must validate your email address for this account before logging in via OpenID'));
+ }
+ }
+ else {
+ // Register new user
+ $form_state['redirect'] = NULL;
+ $form_state['values']['name'] = (empty($response['openid.sreg.nickname'])) ? $identity : $response['openid.sreg.nickname'];
+ $form_state['values']['mail'] = (empty($response['openid.sreg.email'])) ? '' : $response['openid.sreg.email'];
+ $form_state['values']['pass'] = user_password();
+ $form_state['values']['status'] = variable_get('user_register', 1) == 1;
+ $form_state['values']['response'] = $response;
+ $form_state['values']['auth_openid'] = $identity;
+ $form = drupal_retrieve_form('user_register', $form_state);
+ drupal_prepare_form('user_register', $form, $form_state);
+ drupal_validate_form('user_register', $form, $form_state);
+ if (form_get_errors()) {
+ // We were unable to register a valid new user, redirect to standard
+ // user/register and prefill with the values we received.
+ drupal_set_message(t('OpenID registration failed for the reasons listed. You may register now, or if you already have an account you can <a href="@login">log in</a> now and add your OpenID under "My Account"', array('@login' => url('user/login'))), 'error');
+ $_SESSION['openid'] = $form_state['values'];
+ // We'll want to redirect back to the same place.
+ $destination = drupal_get_destination();
+ unset($_REQUEST['destination']);
+ drupal_goto('user/register', $destination);
+ }
+ else {
+ unset($form_state['values']['response']);
+ $account = user_save('', $form_state['values']);
+ user_external_login($account);
+ }
+ drupal_redirect_form($form, $form_state['redirect']);
+ }
+ drupal_goto();
+}
+
+function openid_association_request($public) {
+ include_once drupal_get_path('module', 'openid') .'/openid.inc';
+
+ $request = array(
+ 'openid.ns' => OPENID_NS_2_0,
+ 'openid.mode' => 'associate',
+ 'openid.session_type' => 'DH-SHA1',
+ 'openid.assoc_type' => 'HMAC-SHA1'
+ );
+
+ if ($request['openid.session_type'] == 'DH-SHA1' || $request['openid.session_type'] == 'DH-SHA256') {
+ $cpub = _openid_dh_long_to_base64($public);
+ $request['openid.dh_consumer_public'] = $cpub;
+ }
+
+ return $request;
+}
+
+function openid_authentication_request($claimed_id, $identity, $return_to = '', $assoc_handle = '', $version = 2) {
+ include_once drupal_get_path('module', 'openid') .'/openid.inc';
+
+ $realm = ($return_to) ? $return_to : url('', array('absolute' => TRUE));
+
+ $ns = ($version == 2) ? OPENID_NS_2_0 : OPENID_NS_1_0;
+ $request = array(
+ 'openid.ns' => $ns,
+ 'openid.mode' => 'checkid_setup',
+ 'openid.identity' => $identity,
+ 'openid.claimed_id' => $claimed_id,
+ 'openid.assoc_handle' => $assoc_handle,
+ 'openid.return_to' => $return_to,
+ );
+
+ if ($version == 2) {
+ $request['openid.realm'] = $realm;
+ }
+ else {
+ $request['openid.trust_root'] = $realm;
+ }
+
+ // Simple Registration
+ $request['openid.sreg.required'] = 'nickname,email';
+ $request['openid.ns.sreg'] = "http://openid.net/extensions/sreg/1.1";
+
+ $request = array_merge($request, module_invoke_all('openid', 'request', $request));
+
+ return $request;
+}
+
+/**
+ * Attempt to verify the response received from the OpenID Provider.
+ *
+ * @param $op_endpoint The OpenID Provider URL.
+ * @param $response Array of repsonse values from the provider.
+ *
+ * @return boolean
+ */
+function openid_verify_assertion($op_endpoint, $response) {
+ include_once drupal_get_path('module', 'openid') .'/openid.inc';
+
+ $valid = FALSE;
+
+ $association = db_fetch_object(db_query("SELECT * FROM {openid_association} WHERE assoc_handle = '%s'", $response['openid.assoc_handle']));
+ if ($association && isset($association->session_type)) {
+ $keys_to_sign = explode(',', $response['openid.signed']);
+ $self_sig = _openid_signature($association, $response, $keys_to_sign);
+ if ($self_sig == $response['openid.sig']) {
+ $valid = TRUE;
+ }
+ else {
+ $valid = FALSE;
+ }
+ }
+ else {
+ $request = $response;
+ $request['openid.mode'] = 'check_authentication';
+ $message = _openid_create_message($request);
+ $headers = array('Content-Type' => 'application/x-www-form-urlencoded; charset=utf-8');
+ $result = drupal_http_request($op_endpoint, $headers, 'POST', _openid_encode_message($message));
+ if (!isset($result->error)) {
+ $response = _openid_parse_message($result->data);
+ if (strtolower(trim($response['is_valid'])) == 'true') {
+ $valid = TRUE;
+ }
+ else {
+ $valid = FALSE;
+ }
+ }
+ }
+
+ return $valid;
+}
diff --git a/modules/openid/openid.schema b/modules/openid/openid.schema
new file mode 100644
index 000000000..668b8b8ec
--- /dev/null
+++ b/modules/openid/openid.schema
@@ -0,0 +1,19 @@
+<?php
+// $Id$
+
+function openid_schema() {
+ $schema['openid_association'] = array(
+ 'fields' => array(
+ 'idp_endpoint_uri' => array('type' => 'varchar', 'length' => 255),
+ 'assoc_handle' => array('type' => 'varchar', 'length' => 255),
+ 'assoc_type' => array('type' => 'varchar', 'length' => 32),
+ 'session_type' => array('type' => 'varchar', 'length' => 32),
+ 'mac_key' => array('type' => 'varchar', 'length' => 255),
+ 'created' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+ 'expires_in' => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
+ ),
+ 'primary key' => array('assoc_handle'),
+ );
+
+ return $schema;
+} \ No newline at end of file
diff --git a/modules/openid/xrds.inc b/modules/openid/xrds.inc
new file mode 100644
index 000000000..28fc979c4
--- /dev/null
+++ b/modules/openid/xrds.inc
@@ -0,0 +1,79 @@
+<?php
+// $Id$
+
+// Global variables to track parsing state
+$xrds_open_elements = array();
+$xrds_services = array();
+$xrds_current_service = array();
+
+/**
+ * Main entry point for parsing XRDS documents
+ */
+function xrds_parse($xml) {
+ global $xrds_services;
+
+ $parser = xml_parser_create_ns();
+ xml_set_element_handler($parser, '_xrds_element_start', '_xrds_element_end');
+ xml_set_character_data_handler($parser, '_xrds_cdata');
+
+ xml_parse($parser, $xml);
+ xml_parser_free($parser);
+
+ return $xrds_services;
+}
+
+/**
+ * Parser callback functions
+ */
+function _xrds_element_start(&$parser, $name, $attribs) {
+ global $xrds_open_elements;
+
+ $xrds_open_elements[] = _xrds_strip_namespace($name);
+}
+
+function _xrds_element_end(&$parser, $name) {
+ global $xrds_open_elements, $xrds_services, $xrds_current_service;
+
+ $name = _xrds_strip_namespace($name);
+ if ($name == 'SERVICE') {
+ if (in_array(OPENID_NS_2_0 .'/signon', $xrds_current_service['types']) ||
+ in_array(OPENID_NS_2_0 .'/server', $xrds_current_service['types'])) {
+ $xrds_current_service['version'] = 2;
+ }
+ elseif (in_array(OPENID_NS_1_1, $xrds_current_service['types']) ||
+ in_array(OPENID_NS_1_0, $xrds_current_service['types'])) {
+ $xrds_current_service['version'] = 1;
+ }
+ if (!empty($xrds_current_service['version'])) {
+ $xrds_services[] = $xrds_current_service;
+ }
+ $xrds_current_service = array();
+ }
+ array_pop($xrds_open_elements);
+}
+
+function _xrds_cdata(&$parser, $data) {
+ global $xrds_open_elements, $xrds_services, $xrds_current_service;
+ $path = strtoupper(implode('/', $xrds_open_elements));
+ switch ($path) {
+ case 'XRDS/XRD/SERVICE/TYPE':
+ $xrds_current_service['types'][] = $data;
+ break;
+ case 'XRDS/XRD/SERVICE/URI':
+ $xrds_current_service['uri'] = $data;
+ break;
+ case 'XRDS/XRD/SERVICE/DELEGATE':
+ $xrds_current_service['delegate'] = $data;
+ break;
+ }
+}
+
+function _xrds_strip_namespace($name) {
+ // Strip namespacing.
+ $pos = strrpos($name, ':');
+ if ($pos !== FALSE) {
+ $name = substr($name, $pos + 1, strlen($name));
+ }
+
+ return $name;
+} \ No newline at end of file