# HG changeset patch # User Dan # Date 1262764643 18000 # Node ID 7e0b422b1725796cae7636d3e400b7f7135a0a25 First working revision. diff -r 000000000000 -r 7e0b422b1725 RadiusAuthentication.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/RadiusAuthentication.php Wed Jan 06 02:57:23 2010 -0500 @@ -0,0 +1,418 @@ +attachHook('login_process_userdata_json', 'return radius_auth_hook($userinfo, $req["level"], $req["remember"]);'); +} + +function radius_auth_hook($userinfo, $level, $remember) +{ + global $db, $session, $paths, $template, $plugins; // Common objects + + // First try to just authenticate the user in RADIUS + require_once(ENANO_ROOT . '/plugins/radiusauth/libradauth.php'); + + $server = getConfig('radius_server', false); + $port = getConfig('radius_port', 1812); + $secret = getConfig('radius_secret', ''); + $method = getConfig('radius_method', 'pap'); + if ( empty($server) ) + // bad server? break out and continue the Enano auth chain + return null; + + // We're ready to do a RADIUS auth attempt + try + { + $radius = new RadiusAuth($server, $secret, $port); + $auth_result = $radius->authenticate($userinfo['username'], $userinfo['password'], $method); + } + catch ( RadiusError $e ) + { + return array( + 'mode' => 'error', + 'error' => "The RADIUS interface returned a technical error." + ); + } + + if ( $auth_result ) + { + // RADIUS authentication was successful. + $username = $db->escape(strtolower($userinfo['username'])); + $q = $db->sql_query("SELECT user_id, password FROM " . table_prefix . "users WHERE " . ENANO_SQLFUNC_LOWERCASE . "(username) = '$username';"); + if ( !$q ) + $db->_die(); + if ( $db->numrows() < 1 ) + { + // This user doesn't exist. + // Is creating it our job? + if ( getConfig('radius_disable_local_auth', 0) == 1 ) + { + // Yep, register him + $email = strtolower($userinfo['username']) . '@' . getConfig('radius_email_domain', 'localhost'); + $random_pass = md5(microtime() . mt_rand()); + // load the language + $session->register_guest_session(); + $reg_result = $session->create_user($userinfo['username'], $random_pass, $email); + if ( $reg_result != 'success' ) + { + // o_O + // Registration failed. + return array( + 'mode' => 'error', + 'error' => 'Your username and password were valid, but there was a problem instanciating your local user account.' + ); + } + // Get user ID + $q = $db->sql_query("SELECT user_id, password FROM " . table_prefix . "users WHERE " . ENANO_SQLFUNC_LOWERCASE . "(username) = '$username';"); + if ( !$q ) + $db->_die(); + if ( $db->numrows() < 1 ) + return array( + 'mode' => 'error', + 'error' => 'Your username and password were valid, but there was a problem getting your user ID.' + ); + $row = $db->fetchrow(); + $db->free_result(); + // Quick - lock the account + $q = $db->sql_query('UPDATE ' . table_prefix . "users SET password = 'Locked by RADIUS plugin', password_salt = 'Locked by RADIUS plugin' WHERE user_id = {$row['user_id']};"); + if ( !$q ) + $db->_die(); + + $row['password'] = 'Locked by RADIUS plugin'; + } + else + { + // Nope. Just let Enano fail it properly. + return null; + } + } + else + { + $row = $db->fetchrow(); + $db->free_result(); + } + + $session->register_session($row['user_id'], $userinfo['username'], $row['password'], $level, $remember); + return true; + } + else + { + // RADIUS authentication failed. + + // Are local logons allowed? + if ( getConfig('radius_disable_local_auth', 0) == 0 ) + { + // Yes, allow auth to continue + return null; + } + + // Block the login attempt unless the username is a local admin. + $username = $db->escape(strtolower($userinfo['username'])); + $q = $db->sql_query("SELECT user_level FROM " . table_prefix . "users WHERE " . ENANO_SQLFUNC_LOWERCASE . "(username) = '$username';"); + if ( !$q ) + $db->_die(); + if ( $db->numrows() > 0 ) + { + // Well, the user exists... + list($ul) = $db->fetchrow_num(); + $db->free_result(); + if ( $ul >= USER_LEVEL_ADMIN ) + { + // They're an admin, allow local logon + return null; + } + } + $db->free_result(); + + // User doesn't exist, or is not an admin, and users are not allowed to log on locally. Lock them out. + $q = $db->sql_query('INSERT INTO ' . table_prefix . "lockout(ipaddr, timestamp, action, username)\n" + . " VALUES('" . $db->escape($_SERVER['REMOTE_ADDR']) . "', " . time() . ", 'credential', '" . $db->escape($userinfo['username']) . "');"); + if ( !$q ) + $db->_die(); + + return array( + 'mode' => 'error', + 'error' => 'Invalid RADIUS authentication credentials.' + ); + } +} + +// Registration blocking hook +if ( getConfig('radius_disable_local_auth', 0) == 1 ) +{ + $plugins->attachHook('ucp_register_validate', 'radius_auth_reg_block($error);'); +} + +function radius_auth_reg_block(&$error) +{ + $error = 'Registration on this website is disabled because RADIUS authentication is configured. Please log in using a valid RADIUS username and password, and an account will be created for you automatically.'; +} + +// +// ADMIN +// + +$plugins->attachHook('session_started', 'radius_session_hook();'); + +if ( getConfig('radius_disable_local_auth', 0) == 1 ) +{ + $plugins->attachHook('common_post', 'radius_tou_hook();'); +} + +function radius_session_hook() +{ + global $db, $session, $paths, $template, $plugins; // Common objects + + // Register the admin page + $paths->addAdminNode('adm_cat_security', 'RADIUS Authentication', 'RadiusConfig'); + + // Disable password change + if ( getConfig('radius_disable_local_auth', 0) == 1 && $session->user_level < USER_LEVEL_ADMIN ) + { + $link_text = getConfig('radius_password_text', false); + if ( empty($link_text) ) + $link_text = false; + $link_url = str_replace('%u', $session->username, getConfig('radius_password_url', '')); + if ( empty($link_url) ) + $link_url = false; + $session->disable_password_change($link_url, $link_text); + } +} + +function radius_tou_hook() +{ + global $db, $session, $paths, $template, $plugins; // Common objects + + // Are we pending TOU acceptance? + if ( $session->user_logged_in && !$session->on_critical_page() && trim(getConfig('register_tou', '')) != '' ) + { + $q = $db->sql_query('SELECT account_active FROM ' . table_prefix . "users WHERE user_id = $session->user_id;"); + if ( !$q ) + $db->_die(); + + list($active) = $db->fetchrow_num(); + $db->free_result(); + if ( $active == 1 ) + { + // Pending TOU accept + // Basically, what we do here is force the user to accept the TOU and record it by setting account_active to 2 instead of a 1 + // A bit of a hack, but hey, it works, at least in 1.1.8. + // In 1.1.7, it just breaks your whole account, and $session->on_critical_page() is broken in 1.1.7 so you won't even be able + // to go the admin CP and re-activate yourself. Good times... erhm, sorry. + + if ( isset($_POST['tou_agreed']) && $_POST['tou_agreed'] === 'I accept the terms and conditions displayed on this site' ) + { + // Accepted + $q = $db->sql_query('UPDATE ' . table_prefix . "users SET account_active = 2 WHERE user_id = $session->user_id;"); + if ( !$q ) + $db->_die(); + + return true; + } + + global $output, $lang; + $output->set_title('Terms of Use'); + $output->header(); + + ?> +

Please read and accept the following terms:

+ +
+ +
+ +
+

+ +

+

+ +

+
+ + footer(); + + $db->close(); + exit; + } + } +} + +function page_Admin_RadiusConfig() +{ + // Security check + global $db, $session, $paths, $template, $plugins; // Common objects + if ( $session->auth_level < USER_LEVEL_ADMIN ) + return false; + + if ( isset($_POST['submit']) ) + { + setConfig('radius_enable', isset($_POST['radius_enable']) ? '1' : '0'); + setConfig('radius_server', $_POST['radius_server']); + setConfig('radius_port', intval($_POST['radius_port']) > 0 && intval($_POST['radius_port']) < 65535 ? intval($_POST['radius_port']) : 1812 ); + setConfig('radius_secret', $_POST['radius_secret']); + setConfig('radius_disable_local_auth', isset($_POST['radius_disable_local_auth']) ? '1' : '0'); + setConfig('radius_password_text', $_POST['radius_password_text']); + setConfig('radius_password_url', $_POST['radius_password_url']); + setConfig('radius_email_domain', $_POST['radius_email_domain']); + setConfig('radius_method', $_POST['radius_method']); + + echo '
Your changes have been saved.
'; + } + + acp_start_form(); + ?> +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ RADIUS Authentication Configuration +
+ Enable RADIUS authentication: + + +
+ RADIUS server: + + + Port: + +
+ Shared secret: + + +
+ Authentication method: + + +
+ Enforce RADIUS for single-sign-on:
+ Use this option to force RADIUS passwords and accounts to be used, regardless of local accounts, except for administrators. +
+ +
+ E-mail address domain for autoregistered users:
+ When a user is automatically registered, this domain will be used as the domain for their e-mail address. This way, activation e-mails will + (ideally) reach the user. +
+ +
+ External password management link:
+ Enter a URL here to link to from Enano's Change Password page. Leave blank to not display a link. The text "%u" will be replaced with the user's username. +
+ Link text:
+ Link URL: +
+ +
+
+ '; +} diff -r 000000000000 -r 7e0b422b1725 radiusauth/libmschap.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/radiusauth/libmschap.php Wed Jan 06 02:57:23 2010 -0500 @@ -0,0 +1,101 @@ + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The author of this file respectfully requests that you refrain from +relicensing it under the GPL, although the BSD license permits you to do so. + + $Id: mschap.php,v 1.5 2003/01/26 20:31:11 mbretter Exp $ +*/ + +function des_encrypt_ecb($key, $clearText) +{ + return mcrypt_ecb (MCRYPT_DES, $key, $clearText, MCRYPT_ENCRYPT, str_pad("", 8, chr(0x00))); +} + +function NtPasswordHash($plain) +{ + return mhash (MHASH_MD4, str2unicode($plain)); +} + +function str2unicode($str) +{ + $uni = ''; + for ($i=0;$i +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. The names of the authors may not be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The author of this file respectfully requests that you refrain from +relicensing it under the GPL, although the BSD license permits you to do so. + +*/ + +class RadiusAuth +{ + var $radius; + + var $server = 'localhost'; + var $secret = 's3cr35SHH'; + var $port = 1812; + + function __construct($server, $secret, $port = 1812) + { + $this->radius = radius_auth_open(); + if ( !$this->radius ) + { + throw new RadiusError("Could not get RADIUS resource"); + } + + $this->set_server_params($server, $secret, $port); + $this->create_request(); + } + + function set_server_params($server, $secret, $port = 1812) + { + $this->server = $server; + $this->secret = $secret; + $this->port = $port; + } + + function create_request() + { + if ( !radius_add_server($this->radius, $this->server, $this->port, $this->secret, 3, 3) ) + throw new RadiusError(radius_strerror($this->radius)); + + if ( !radius_create_request($this->radius, RADIUS_ACCESS_REQUEST) ) + throw new RadiusError(radius_strerror($this->radius)); + + if (!radius_put_string($this->radius, RADIUS_NAS_IDENTIFIER, isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost')) + throw new RadiusError(radius_strerror($this->radius)); + + /* + if (!radius_put_int($this->radius, RADIUS_SERVICE_TYPE, RADIUS_FRAMED)) + throw new RadiusError(radius_strerror($this->radius)); + + if (!radius_put_int($this->radius, RADIUS_FRAMED_PROTOCOL, RADIUS_PPP)) + throw new RadiusError(radius_strerror($this->radius)); + */ + + if (!radius_put_string($this->radius, RADIUS_CALLING_STATION_ID, isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1')) + throw new RadiusError(radius_strerror($this->radius)); + } + + function authenticate($username, $password, $method = 'pap') + { + // + // Send the username + // + + if ( !radius_put_string($this->radius, RADIUS_USER_NAME, $username) ) + throw new RadiusError("RADIUS_USER_NAME: " . radius_strerror($this->radius)); + + // + // Send the password, or complete the challenge process + // + + switch ( $method ) + { + case 'chap': + + // + // CHAP + // + + /* generate Challenge */ + mt_srand(time() * mt_rand()); + $chall = mt_rand(); + + // FYI: CHAP = md5(ident + plaintextpass + challenge) + $chapval = pack('H*', md5(pack('Ca*', 1, $password . $chall))); + // Radius wants the CHAP Ident in the first byte of the CHAP-Password + $pass_chap = pack('C', 1) . $chapval; + + if (!radius_put_attr($this->radius, RADIUS_CHAP_PASSWORD, $pass_chap)) + throw new RadiusError(radius_strerror($this->radius)); + + if (!radius_put_attr($this->radius, RADIUS_CHAP_CHALLENGE, $chall)) + throw new RadiusError(radius_strerror($this->radius)); + + break; + + case 'mschap': + + // + // MS-CHAP v1 + // + + require_once(ENANO_ROOT . '/plugins/radiusauth/libradauth.php'); + + $challenge = GenerateChallenge(); + + if (!radius_put_vendor_attr($this->radius, RADIUS_VENDOR_MICROSOFT, RADIUS_MICROSOFT_MS_CHAP_CHALLENGE, $challenge)) + throw new RadiusError(radius_strerror($this->radius)); + + $ntresp = ChallengeResponse($challenge, NtPasswordHash($password)); + $lmresp = str_repeat ("\0", 24); + + // Response: chapid, flags (1 = use NT Response), LM Response, NT Response + $resp = pack('CCa48',1 , 1, $lmresp . $ntresp); + + if ( !radius_put_vendor_attr($this->radius, RADIUS_VENDOR_MICROSOFT, RADIUS_MICROSOFT_MS_CHAP_RESPONSE, $resp)) + throw new RadiusError(radius_strerror($this->radius)); + + break; + + case 'mschapv2': + + // + // MS-CHAP v2 + // + + require_once(ENANO_ROOT . '/plugins/radiusauth/libradauth.php'); + + $authChallenge = GenerateChallenge(16); + + if (!radius_put_vendor_attr($this->radius, RADIUS_VENDOR_MICROSOFT, RADIUS_MICROSOFT_MS_CHAP_CHALLENGE, $authChallenge)) + throw new RadiusError(radius_strerror($this->radius)); + + // we have no client, therefore we generate the Peer-Challenge + $peerChallenge = GeneratePeerChallenge(); + + $ntresp = GenerateNTResponse($authChallenge, $peerChallenge, $username, $password); + $reserved = str_repeat ("\0", 8); + + // Response: chapid, flags (1 = use NT Response), Peer challenge, reserved, Response + $resp = pack('CCa16a8a24',1 , 1, $peerChallenge, $reserved, $ntresp); + + if (!radius_put_vendor_attr($this->radius, RADIUS_VENDOR_MICROSOFT, RADIUS_MICROSOFT_MS_CHAP2_RESPONSE, $resp)) + throw new RadiusError(radius_strerror($this->radius)); + + break; + + case 'pap': + default: + + // + // PAP + // + + if ( !radius_put_string($this->radius, RADIUS_USER_PASSWORD, $password) ) + throw new RadiusError("RADIUS_USER_PASSWORD: " . radius_strerror($this->radius)); + + break; + } + + $req = radius_send_request($this->radius); + if ( !$req ) + throw new RadiusError(radius_strerror($this->radius)); + + switch($req) + { + case RADIUS_ACCESS_ACCEPT: + return true; + + case RADIUS_ACCESS_REJECT: + return false; + + default: + echo "Unexpected return value:$req\n
"; + return false; + } + } + + function get_attrs() + { + $attrs = array(); + while ($resa = radius_get_attr($this->radius)) + { + $attrs[ $resa['attr'] ] = $resa['data']; + } + + return $attrs; + } + + function get_authenticator() + { + if ( $authent = radius_request_authenticator($this->radius) ) + return $authent; + + throw new RadiusError(radius_strerror($this->radius)); + } + + function close() + { + radius_close($this->radius); + } +} + +class RadiusError extends Exception +{ + +}