diff options
author | Dries Buytaert <dries@buytaert.net> | 2007-06-18 16:09:39 +0000 |
---|---|---|
committer | Dries Buytaert <dries@buytaert.net> | 2007-06-18 16:09:39 +0000 |
commit | aa308028aa570d15d3abf9c7679d7de16019b78b (patch) | |
tree | c7f87e5423007f088feb99149ebf2d16752631be | |
parent | 8f9298577e7db9af6d34c3d69bd633a5c7e3de74 (diff) | |
download | brdo-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.txt | 1 | ||||
-rw-r--r-- | modules/locale/locale.module | 8 | ||||
-rw-r--r-- | modules/node/node.module | 2 | ||||
-rw-r--r-- | modules/openid/login-bg.png | bin | 0 -> 426 bytes | |||
-rw-r--r-- | modules/openid/openid.css | 32 | ||||
-rw-r--r-- | modules/openid/openid.inc | 420 | ||||
-rw-r--r-- | modules/openid/openid.info | 6 | ||||
-rw-r--r-- | modules/openid/openid.install | 18 | ||||
-rw-r--r-- | modules/openid/openid.js | 29 | ||||
-rw-r--r-- | modules/openid/openid.module | 530 | ||||
-rw-r--r-- | modules/openid/openid.schema | 19 | ||||
-rw-r--r-- | modules/openid/xrds.inc | 79 |
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 Binary files differnew file mode 100644 index 000000000..8eca055f3 --- /dev/null +++ b/modules/openid/login-bg.png 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 |